<?php

namespace Drupal\ai;

use Drupal\Component\Utility\Crypt;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\Site\Settings;
use Symfony\Component\HttpFoundation\ParameterBag;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;

/**
 * A value object for the ai tools library state.
 *
 * When the ai tools library is opened it needs several parameters to work
 * properly. These parameters are normally extracted from the current URL, then
 * retrieved from and managed by the AiToolsLibraryState value object. The
 * following parameters are required in order to open the ai tools library:
 * - ai_tools_library_opener_id: The ID of a container service which implements
 *   \Drupal\ai\AiToolsLibraryOpenerInterface and is responsible for
 *   interacting with the ai tools library on behalf of the "thing" (e.g., a
 *   field widget or text editor button) which opened it.
 * - ai_library_allowed_groups: The tools groups available in the library can
 *   be restricted to a list of allowed groups. This should be an array of group
 *   IDs.
 * - ai_tools_library_selected_group: The library contains tabs to navigate
 *   between the different tools groups. The selected group contains the ID of
 *   the group type whose tab that should be opened.
 *
 * This object can also carry an optional opener-specific array of arbitrary
 * values, under the ai_tools_library_opener_parameters key. These values are
 * included in the hash generated by ::getHash(), so the end user cannot tamper
 * with them either.
 *
 * @see \Drupal\ai\AiToolsLibraryOpenerInterface
 */
class AiToolsLibraryState extends ParameterBag implements CacheableDependencyInterface {

  /**
   * {@inheritdoc}
   */
  public function __construct(array $parameters = []) {
    $this->validateRequiredParameters($parameters['ai_tools_library_opener_id'], $parameters['ai_tools_library_allowed_groups'], $parameters['ai_tools_library_selected_group']);
    $parameters += [
      'ai_tools_library_opener_parameters' => [],
    ];
    parent::__construct($parameters);
    $this->set('hash', $this->getHash());
  }

  /**
   * Creates a new AiToolsLibraryState object.
   *
   * @param string $opener_id
   *   The opener ID.
   * @param string[] $allowed_group_ids
   *   The allowed group IDs.
   * @param string $selected_group_id
   *   The selected group ID.
   * @param array $opener_parameters
   *   (optional) Any additional opener-specific parameter values.
   *
   * @return static
   *   A state object.
   */
  public static function create($opener_id, array $allowed_group_ids, $selected_group_id, array $opener_parameters = []) {
    $state = new static([
      'ai_tools_library_opener_id' => $opener_id,
      'ai_tools_library_allowed_groups' => $allowed_group_ids,
      'ai_tools_library_selected_group' => $selected_group_id,
      'ai_tools_library_opener_parameters' => $opener_parameters,
    ]);
    return $state;
  }

  /**
   * Get the ai tools library state from a request.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request.
   *
   * @return static
   *   A state object.
   *
   * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
   *   Thrown when the hash query parameter is invalid.
   */
  public static function fromRequest(Request $request) {
    $query = $request->query;

    // Create a AiToolsLibraryState object through the create method to make
    // sure all validation runs.
    $state = static::create(
      $query->get('ai_tools_library_opener_id'),
      $query->all('ai_tools_library_allowed_groups'),
      $query->get('ai_tools_library_selected_group'),
      $query->all('ai_tools_library_opener_parameters')
    );

    // The request parameters need to contain a valid hash to prevent a
    // malicious user modifying the query string to attempt to access
    // inaccessible information.
    if (!$state->isValidHash($query->get('hash'))) {
      throw new BadRequestHttpException("Invalid ai tools library parameters specified.");
    }

    // Once we have validated the required parameters, we restore the parameters
    // from the request since there might be additional values.
    $state->replace($query->all());
    return $state;
  }

  /**
   * Validates the required parameters for a new AiToolsLibraryState object.
   *
   * @param string $opener_id
   *   The opener ID.
   * @param string[] $allowed_group_ids
   *   The allowed group IDs.
   * @param string $selected_group_id
   *   The selected group ID.
   *
   * @throws \InvalidArgumentException
   *   If one of the passed arguments is missing or does not pass the
   *   validation.
   */
  protected function validateRequiredParameters($opener_id, array $allowed_group_ids, $selected_group_id) {
    // The opener ID must be a non-empty string.
    if (!is_string($opener_id) || empty(trim($opener_id))) {
      throw new \InvalidArgumentException('The opener ID parameter is required and must be a string.');
    }

    // The allowed group type IDs must be an array of non-empty strings.
    if (empty($allowed_group_ids) || !is_array($allowed_group_ids)) {
      throw new \InvalidArgumentException('The allowed groups parameter is required and must be an array of strings.');
    }
    foreach ($allowed_group_ids as $allowed_group_id) {
      if (!is_string($allowed_group_id) || empty(trim($allowed_group_id))) {
        throw new \InvalidArgumentException('The allowed types parameter is required and must be an array of strings.');
      }
    }

    // The selected type ID must be a non-empty string.
    if (!is_string($selected_group_id) || empty(trim($selected_group_id))) {
      throw new \InvalidArgumentException('The selected type parameter is required and must be a string.');
    }
    // The selected type ID must be present in the list of allowed types.
    if (!in_array($selected_group_id, $allowed_group_ids, TRUE)) {
      throw new \InvalidArgumentException('The selected type parameter must be present in the list of allowed types.');
    }
  }

  /**
   * Get the hash for the state object.
   *
   * @return string
   *   The hashed parameters.
   */
  public function getHash() {
    // Create a hash from the required state parameters and the serialized
    // optional opener-specific parameters. Sort the allowed types and
    // opener parameters so that differences in order do not result in
    // different hashes.
    $allowed_group_ids = array_values($this->getAllowedGroupIds());
    sort($allowed_group_ids);
    $opener_parameters = $this->getOpenerParameters();
    ksort($opener_parameters);
    $hash = implode(':', [
      $this->getOpenerId(),
      implode(':', $allowed_group_ids),
      $this->getSelectedGroupId(),
      serialize($opener_parameters),
    ]);
    // @phpstan-ignore-next-line
    return Crypt::hmacBase64($hash, \Drupal::service('private_key')->get() . Settings::getHashSalt());
  }

  /**
   * Validate a hash for the state object.
   *
   * @param string $hash
   *   The hash to validate.
   *
   * @return string
   *   The hashed parameters.
   */
  public function isValidHash($hash) {
    return hash_equals($this->getHash(), $hash);
  }

  /**
   * Returns the ID of the ai tools library opener service.
   *
   * @return string
   *   The ai tools library opener service ID.
   */
  public function getOpenerId() {
    return $this->get('ai_tools_library_opener_id');
  }

  /**
   * Returns the group IDs which can be selected.
   *
   * @return string[]
   *   The group IDs.
   */
  public function getAllowedGroupIds() {
    return $this->all('ai_tools_library_allowed_groups');
  }

  /**
   * Returns the selected group.
   *
   * @return string
   *   The selected group.
   */
  public function getSelectedGroupId() {
    return $this->get('ai_tools_library_selected_group');
  }

  /**
   * Returns all opener-specific parameter values.
   *
   * @return array
   *   An associative array of all opener-specific parameter values.
   */
  public function getOpenerParameters() {
    return $this->all('ai_tools_library_opener_parameters');
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheContexts() {
    return ['url.query_args'];
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheMaxAge() {
    return Cache::PERMANENT;
  }

  /**
   * {@inheritdoc}
   */
  public function getCacheTags() {
    return [];
  }

}
