<?php

namespace Drupal\acquia_contenthub\Commands;

use Acquia\ContentHubClient\CDF\CDFObjectInterface;
use Consolidation\AnnotatedCommand\CommandError;
use Drupal\acquia_contenthub\Commands\Traits\ColorizedOutputTrait;
use Drupal\acquia_contenthub\ContentHubCommonActions;
use Drupal\acquia_contenthub\Exception\EntityOriginMismatchException;
use Drupal\Component\Diff\Diff;
use Drupal\Component\Diff\DiffFormatter;
use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\depcalc\DependencyCalculator;
use Drush\Commands\DrushCommands;

/**
 * Drush commands for interacting with Acquia Content Hub entities.
 *
 * @package Drupal\acquia_contenthub\Commands
 */
class AcquiaContentHubEntityCommands extends DrushCommands {

  use ColorizedOutputTrait;

  /**
   * The dependency calculator.
   *
   * @var \Drupal\depcalc\DependencyCalculator
   */
  protected DependencyCalculator $calculator;

  /**
   * The Content Hub Common Actions.
   *
   * @var \Drupal\acquia_contenthub\ContentHubCommonActions
   */
  protected ContentHubCommonActions $commonActions;

  /**
   * The entity repository.
   *
   * @var \Drupal\Core\Entity\EntityRepositoryInterface
   */
  protected EntityRepositoryInterface $entityRepository;

  /**
   * Retrieves the entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected EntityTypeManagerInterface $entityTypeManager;

  /**
   * AcquiaContentHubEntityCommands constructor.
   *
   * @param \Drupal\depcalc\DependencyCalculator $calculator
   *   The dependency calculator.
   * @param \Drupal\acquia_contenthub\ContentHubCommonActions $common_actions
   *   The Content Hub Common Actions service.
   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
   *   The entity repository service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager service.
   */
  public function __construct(DependencyCalculator $calculator, ContentHubCommonActions $common_actions, EntityRepositoryInterface $entity_repository, EntityTypeManagerInterface $entity_type_manager) {
    $this->calculator = $calculator;
    $this->commonActions = $common_actions;
    $this->entityRepository = $entity_repository;
    $this->entityTypeManager = $entity_type_manager;
  }

  /**
   * Retrieves an Entity from a local source or contenthub.
   *
   * @param string $op
   *   The operation being performed.
   * @param string $uuid
   *   Entity identifier or entity's UUID.
   * @param string $entity_type
   *   The entity type in case of local retrieval.
   * @param array $options
   *   An associative array of options. 'decode', Decodes the metadata 'data'
   *   element to make it easier to understand the content of each CDF
   *   entity stored in Content Hub.
   *
   * @throws \Exception
   */
  public function contenthubEntity(string $op, string $uuid, ?string $entity_type = NULL, array $options = ['decode' => NULL]): void {
    if (empty($uuid)) {
      throw new \Exception("Please supply the uuid of the entity you want to retrieve.");
    }

    switch ($op) {
      case 'local':
        if (empty($entity_type)) {
          throw new \Exception(dt("Entity_type is required for local entities"));
        }
        $entity = $this->entityRepository->loadEntityByUuid($entity_type, $uuid);

        $entities = [];
        $objects = $this->commonActions->getEntityCdf($entity, $entities, FALSE, TRUE);
        $data = [];
        foreach ($objects as $object) {
          $data[$object->getUuid()] = $object->toArray();
          // Decode the base64 'data' element in 'metadata'.
          if ($options['decode']) {
            $this->decodeEntityArrayMetadata($data[$object->getUuid()]);
          }
        }
        $json = json_encode(
          $data,
          JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES
        );
        $this->output()->writeln($json);
        break;

      case 'remote':
        $entity = $this->commonActions->getRemoteEntity($uuid);
        if (!$entity) {
          $this->output()->writeln($this->toYellow('Entity could not be found in Content Hub.'));
          return;
        }
        $entity_array = $entity->toArray();
        // Decode the base64 'data' element in 'metadata'.
        if ($options['decode']) {
          $this->decodeEntityArrayMetadata($entity_array);
        }
        elseif (!$entity instanceof CDFObjectInterface) {
          return;
        }
        $json = json_encode($entity_array, JSON_PRETTY_PRINT |
          JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
        $this->output()->writeln($json);
        break;

      default:
        // Invalid operation.
        throw new \Exception(dt('The op "@op" is invalid', ['@op' => $op]));
    }
  }

  /**
   * Prints the CDF from a local source (drupal site)
   *
   * @param string $entity_id
   *   The entity identifier or entity's UUID.
   * @param string $entity_type
   *   The entity type to load. (Optional if you are passing entity uuid.)
   * @param array $options
   *   An associative array of options whose values come from cli, aliases,
   *   config, etc.
   *
   * @option decode
   *   Decodes the metadata 'data' element to make it easier to understand the
   *   content of each CDF entity stored in Content Hub.
   *
   * @usage ach-lo e84aa78a-873d-4a4a-b9d0-cde24c0020a6
   *   Prints CDF of given entity uuid
   * @usage ach-lo 1 node
   *   Prints CDF of entity id 1 and entity type node.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   *
   * @command acquia:contenthub-local
   * @aliases ach-lo,acquia-contenthub-local
   */
  public function contenthubLocal($entity_id, $entity_type = NULL, array $options = ['decode' => NULL]): ?CommandError {
    if ($entity_type !== NULL && !$this->entityTypeManager->hasDefinition($entity_type)) {
      return new CommandError(sprintf(
        'Entity type %s does not exist.', $entity_type
      ));
    }

    $entity = $this->loadEntity($entity_id, $entity_type);
    if ($entity) {
      $entity_type = $entity->getEntityTypeId();
      $this->contenthubEntity('local', $entity->uuid(), $entity_type, $options);
      return NULL;
    }

    if (Uuid::isValid($entity_id)) {
      return new CommandError(sprintf(
        'Entity having uuid = %s does not exist.', $entity_id
      ));
    }

    return new CommandError(sprintf(
      'Entity having entity_type = %s and entity_id = %s does not exist.',
      $entity_type,
      $entity_id
    ));
  }

  /**
   * Prints the CDF from a remote source (Content Hub)
   *
   * @param string $uuid
   *   The entity's UUID.
   * @param array $options
   *   An associative array of options whose values come from cli, aliases,
   *   config, etc.
   *
   * @option decode
   *   Decodes the metadata 'data' element to make it easier to understand the
   *   content of each CDF entity stored in Content Hub.
   *
   * @command acquia:contenthub-remote
   * @aliases ach-re,acquia-contenthub-remote
   *
   * @throws \Exception
   */
  public function contenthubRemote($uuid, array $options = ['decode' => NULL]) {
    if (FALSE === Uuid::isValid($uuid)) {
      throw new \Exception(dt("Argument provided is not a UUID."));
    }
    $this->contenthubEntity('remote', $uuid, NULL, $options);
  }

  /**
   * Deletes a single entity from the Content Hub.
   *
   * @param string $uuid
   *   The entity's UUID.
   * @param array $options
   *   An associative array of options whose values come from cli, aliases,
   *   config, etc.
   *
   * @option ignore-origin
   *   Ignores origin check while deleting entity.
   * @option no-propagate
   *   Prevents deletion event from being propagated to subscribers.
   *
   * @usage drush acquia:contenthub-delete 848e7343-c079-4235-9693-0f9e6386c7ed
   *   | Deletes entity by uuid.
   * @usage drush acquia:contenthub-delete 848e7343-c079-4235-9693-0f9e6386c7ed --ignore-origin
   *   | Deletes entity by uuid even if entity source is different.
   * @usage drush acquia:contenthub-delete 848e7343-c079-4235-9693-0f9e6386c7ed --no-propagate
   *   | Entity deletion event will not be propagated to the subscribers.
   *
   * @command acquia:contenthub-delete
   * @aliases ach-del,acquia-contenthub-delete
   *
   * @throws \Exception
   */
  public function contenthubDelete(string $uuid, array $options = ['ignore-origin' => FALSE]): void {
    $this->io()->writeln('Drush Command Documentation <info>https://docs.acquia.com/drupal-starter-kits/add-ons/content-hub/using-drush-content-hub#section-acquiacontenthub-delete</info>.');
    if (!$options['no-propagate']) {
      $this->logger()->warning(
        'On entity deletion, a webhook will be sent around to all subscribers about the deletion.'
      );
    }
    if (!$this->io()->confirm(dt('Are you sure you want to delete the entity with uuid = @uuid from Content Hub? There is no way back from this action!', [
      '@uuid' => $uuid,
    ]))) {
      return;
    }

    try {
      $result = $this->commonActions->deleteRemoteEntity($uuid, $options['ignore-origin'], !$options['no-propagate']);
      if ($result === TRUE) {
        $this->output()->writeln(dt('Entity with UUID = @uuid has been successfully deleted from the Content Hub Service.', [
          '@uuid' => $uuid,
        ]));
        return;
      }
      $this->output()->writeln(dt('WARNING: Entity with UUID = @uuid cannot be deleted from the Content Hub Service.', [
        '@uuid' => $uuid,
      ]));
    }
    catch (EntityOriginMismatchException $e) {
      $this->output()->writeln($e->getMessage() . 'Use --ignore-origin flag to ignore origin check.');
    }
  }

  /**
   * Compares CDF between local and remote entity.
   *
   * @param string $entity_id
   *   The entity identifier or entity's UUID.
   * @param string|null $entity_type
   *   The entity type to load. (Optional if you are passing entity uuid.)
   *
   * @usage ach-cdf-diff e84aa78a-873d-4a4a-b9d0-cde24c0020a6
   *   Shows CDF differences for entity with given UUID
   * @usage ach-cdf-diff 1 node
   *   Shows CDF differences for node with ID 1
   *
   * @command acquia:contenthub-cdf-diff
   * @aliases ach-cdf-diff,acquia-contenthub-cdf-diff
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Exception
   */
  public function contenthubCdfDiff(string $entity_id, $entity_type = NULL): ?CommandError {
    if ($entity_type !== NULL && !$this->entityTypeManager->hasDefinition($entity_type)) {
      return new CommandError(sprintf(
        'Entity type %s does not exist.', $entity_type
      ));
    }

    $entity = $this->loadEntity($entity_id, $entity_type);
    if (!$entity) {
      if (Uuid::isValid($entity_id)) {
        return new CommandError(sprintf(
          'Entity having uuid = %s does not exist locally.', $entity_id
        ));
      }
      return new CommandError(sprintf(
        'Entity having entity_type = %s and entity_id = %s does not exist locally.',
        $entity_type,
        $entity_id
      ));
    }

    $uuid = $entity->uuid();

    $entities = [];
    $local_cdf = $this->commonActions->getEntityCdf($entity, $entities, FALSE);
    $local_data = [];
    foreach ($local_cdf as $object) {
      $local_arr = $object->toArray();
      $this->decodeEntityArrayMetadata($local_arr);
      $local_data[$object->getUuid()] = $local_arr;
    }

    $remote_entity = $this->commonActions->getRemoteEntity($uuid);
    if (!$remote_entity) {
      $this->output()->writeln($this->toYellow('Entity could not be found in Content Hub.'));
      return NULL;
    }
    $remote_arr = $remote_entity->toArray();
    $this->decodeEntityArrayMetadata($remote_arr);
    $remote_data = [$uuid => $remote_arr];

    $this->printDiff($local_data[$uuid], $remote_data[$uuid]);
    return NULL;
  }

  /**
   * Prints the diff between two arrays.
   *
   * @param array $local_data
   *   The local data array.
   * @param array $remote_data
   *   The remote data array.
   */
  protected function printDiff(array $local_data, array $remote_data): void {
    $local_sorted = $this->recursiveKeySort($local_data);
    $remote_sorted = $this->recursiveKeySort($remote_data);

    $local_json = json_encode($local_sorted, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
    $remote_json = json_encode($remote_sorted, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

    $local_arr = explode("\n", $local_json);
    $remote_arr = explode("\n", $remote_json);

    $header = "--- Local\n+++ Remote\n";
    // @todo remove this condition when minimum supported version of drupal is 10.1.
    if (class_exists('SebastianBergmann\Diff\Differ') &&
      class_exists('SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder')) {

      $outputBuilder = new \SebastianBergmann\Diff\Output\UnifiedDiffOutputBuilder($header); // phpcs:ignore
      $differ = new \SebastianBergmann\Diff\Differ($outputBuilder); // phpcs:ignore
      $unified = $differ->diff($local_json, $remote_json);
    }
    else {
      $diff = new Diff($local_arr, $remote_arr);
      $formatter = new DiffFormatter();
      $unified = $formatter->format($diff);
    }

    if (empty($unified) || $unified === $header) {
      $this->output()->writeln($this->toGreen('No differences found between local and remote CDF.'));
      return;
    }

    $this->output()->writeln($this->toRed('=== CDF UNIFIED DIFF ==='));
    $this->output()->writeln($unified);
  }

  /**
   * Recursively sort array keys.
   *
   * @param mixed $data
   *   Array or value to sort.
   *
   * @return mixed
   *   Sorted array or value.
   */
  protected function recursiveKeySort($data) {
    if (is_array($data)) {
      foreach ($data as $key => $value) {
        $data[$key] = $this->recursiveKeySort($value);
      }
      ksort($data);
    }
    return $data;
  }

  /**
   * Decodes the base64 'data' element inside a CDF entity 'metadata'.
   *
   * @param array $cdf_entity
   *   The CDF entity array before it is written to the output.
   */
  protected function decodeEntityArrayMetadata(array &$cdf_entity) {
    $types = [
      'drupal8_content_entity',
      'drupal8_config_entity',
      'rendered_entity',
    ];
    if (in_array($cdf_entity['type'], $types)) {
      $cdf_entity['metadata']['data'] = base64_decode($cdf_entity['metadata']['data']);
    }
  }

  /**
   * Searches for entity using entity uuid.
   *
   * @param string $uuid
   *   Entity uuid.
   *
   * @return \Drupal\Core\Entity\EntityInterface|null
   *   Returns entity if available otherwise NULL.
   */
  protected function getEntityByUuid(string $uuid): ?EntityInterface {
    foreach ($this->entityTypeManager->getDefinitions() as $entity_type => $entity_def) {
      if (!$entity_def->hasKey('uuid')) {
        continue;
      }
      $entity = $this->entityRepository->loadEntityByUuid($entity_type, $uuid);
      if (!empty($entity)) {
        return $entity;
      }
    }
    return NULL;
  }

  /**
   * Provides entity if available.
   *
   * @param string $entity_id
   *   Entity id or uuid.
   * @param string|null $entity_type
   *   Entity type.
   *
   * @return \Drupal\Core\Entity\EntityInterface|null
   *   Returns enitity if available.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  protected function loadEntity(string $entity_id, ?string $entity_type): ?EntityInterface {
    if (Uuid::isValid($entity_id)) {
      return $entity_type !== NULL ? $this->entityRepository->loadEntityByUuid($entity_type, $entity_id) :
        $this->getEntityByUuid($entity_id);
    }
    if ($entity_type === NULL) {
      throw new \Exception(dt("Missing required parameter: entity_type"));
    }
    return $this->entityTypeManager->getStorage($entity_type)->load($entity_id);
  }

}
