<?php

declare(strict_types=1);

namespace Drupal\canvas\Controller;

use Drupal\Core\Entity\EntityInterface;
use Drupal\canvas\AutoSave\AutoSaveManager;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;

/**
 * Trait providing client auto-save validation infrastructure for controllers.
 *
 * This trait is used to validate that the client is aware of the latest
 * auto-saves for both:
 * 1. data part of the page preview:
 *    - the component tree (`layout` + `model` in the client-side
 *      representation)
 *    - values for all entity fields besides the Canvas field (aka component
 *      tree)
 *    - PageRegion config entities
 * 2. config entities which are explicitly edited and auto-saved
 *    - JavaScriptComponent config entities
 *    - AssetLibrary config entities.
 *
 * @see \Drupal\canvas\Controller\ApiLayoutController
 * @see \Drupal\canvas\Controller\ApiConfigAutoSaveControllers
 * @internal
 */
trait AutoSaveValidateTrait {

  /**
   * Validates the client has the latest auto-saves for an array of entities.
   *
   * Currently, Drupal Canvas does not support concurrent editing. This
   * function enforces this by ensuring the client has the latest changes as
   * determined by the auto-save data stored on the server.
   *
   * The client must return the 'autoSaves' array that is returned from this
   * trait. The 'autoSaves' array contains a hash of the auto-save data
   * and an 'autoSaveStartingPoint' for all entities that may be updated in the
   * current request. See ::getClientAutoSaveData() for more details.
   *
   * The client also sends a `clientInstanceId` which is a UUID generated by the
   * client that is unique to the client instance. To handle edge cases where a
   * client request is sent before the last request has returned with the latest
   * auto-save hashes, if the `clientInstanceId` matches the value in the
   * server's auto-save entry, then the 'hash' values is 'autoSaves' is ignored
   * and only the 'autoSaveStartingPoint' values must match.
   *
   * @see ::getClientAutoSaveData()
   */
  protected function validateAutoSaves(array $entitiesToValidate, array $autoSaves, string $clientId): void {
    $expected_auto_saves = [];
    $foundClientIdMismatch = FALSE;
    foreach ($entitiesToValidate as $entityToValidate) {
      \assert($entityToValidate instanceof EntityInterface);
      $autoSaveEntity = $this->autoSaveManager->getAutoSaveEntity($entityToValidate);
      $expected_auto_saves[AutoSaveManager::getAutoSaveKey($entityToValidate)] = $this->autoSaveManager->getClientAutoSaveData($entityToValidate);
      $foundClientIdMismatch = $foundClientIdMismatch || (!$autoSaveEntity->isEmpty() && $autoSaveEntity->clientId !== $clientId);
    }
    $expected_auto_saves = array_filter($expected_auto_saves);
    ksort($expected_auto_saves);
    ksort($autoSaves);
    if (!$foundClientIdMismatch) {
      // If the client ID matches, we can skip the check for the auto-saves
      // hashes. Client requests may be sent off before the last request
      // returns.
      // Remove 'hash' from the expected auto-saves and actual ones. But we
      // should still make sure the 'autoSaveStartingPoint' matches.
      array_walk($expected_auto_saves, function (&$value) {
        unset($value['hash']);
      });
      array_walk($autoSaves, function (&$value) {
        unset($value['hash']);
      });
    }

    if ($expected_auto_saves !== $autoSaves) {
      throw new ConflictHttpException('You do not have the latest changes, please refresh your browser.');
    }
  }

  /**
   * @todo Move this logic elsewhere in https://www.drupal.org/project/canvas/issues/3535458
   */
  protected function getAutoSaveHashes(array $entities): array {
    $autoSaveHashes = [];
    foreach ($entities as $entity) {
      \assert($entity instanceof EntityInterface);
      $autoSaveHashes[AutoSaveManager::getAutoSaveKey($entity)] = $this->autoSaveManager->getClientAutoSaveData($entity);
    }
    return array_filter($autoSaveHashes);
  }

}
