<?php

declare(strict_types=1);

namespace Drupal\flowdrop_memory\Service;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\flowdrop_memory\Exception\MemoryBackendException;
use Drupal\flowdrop_memory\Plugin\MemoryBackend\MemoryBackendInterface;

/**
 * Central service for reading and writing workflow memory.
 *
 * Resolves the active backend by the explicit backend ID passed by the caller
 * (typically from a node processor parameter configured by the site admin).
 * Backend instances are cached per backend ID within the request.
 *
 * Inject this service into node processors and custom services that need
 * access to workflow memory:
 *
 * @code
 * public function __construct(private readonly MemoryManager $memory) {}
 *
 * $this->memory->set('pipeline', $pipelineId, 'counter', 42, NULL, 'entity');
 * $value = $this->memory->get('pipeline', $pipelineId, 'counter', 0, 'entity');
 * @endcode
 *
 * @api
 */
class MemoryManager {

  /**
   * Maximum allowed size for a serialized value in bytes (1 MB).
   */
  const MAX_VALUE_BYTES = 1_048_576;

  /**
   * Cache of instantiated backend instances, keyed by backend plugin ID.
   *
   * @var array<string, \Drupal\flowdrop_memory\Plugin\MemoryBackend\MemoryBackendInterface>
   */
  private array $backendCache = [];

  /**
   * Constructs a new MemoryManager.
   *
   * @param \Drupal\flowdrop_memory\Service\MemoryBackendPluginManager $pluginManager
   *   The memory backend plugin manager.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory (used for backend-specific configuration only).
   */
  public function __construct(
    private readonly MemoryBackendPluginManager $pluginManager,
    private readonly ConfigFactoryInterface $configFactory,
  ) {}

  /**
   * Returns the backend instance for a given backend plugin ID.
   *
   * Backend-specific configuration (e.g. cache bin for the cached backend)
   * is read from flowdrop_memory.settings → backend_config.<backendId>.
   *
   * @param string $backendId
   *   The memory backend plugin ID (e.g. 'static', 'cached', 'entity').
   *
   * @return \Drupal\flowdrop_memory\Plugin\MemoryBackend\MemoryBackendInterface
   *   The resolved and instantiated backend plugin.
   */
  public function getBackend(string $backendId = 'static'): MemoryBackendInterface {
    if (isset($this->backendCache[$backendId])) {
      return $this->backendCache[$backendId];
    }

    $config = $this->configFactory->get('flowdrop_memory.settings');
    $backendConfig = (array) ($config->get('backend_config.' . $backendId) ?? []);
    $backend = $this->pluginManager->createInstance($backendId, $backendConfig);
    $this->backendCache[$backendId] = $backend;

    return $backend;
  }

  /**
   * Retrieves a value from memory.
   *
   * @param string $scope
   *   The memory scope.
   * @param string $scope_id
   *   The scope identifier.
   * @param string $key
   *   The memory key.
   * @param mixed $default
   *   Default value if the key does not exist.
   * @param string $backendId
   *   The memory backend plugin ID to use.
   *
   * @return mixed
   *   The stored value or $default.
   */
  public function get(string $scope, string $scope_id, string $key, mixed $default = NULL, string $backendId = 'static'): mixed {
    return $this->getBackend($backendId)->get($scope, $scope_id, $key, $default);
  }

  /**
   * Stores a value in memory.
   *
   * @param string $scope
   *   The memory scope.
   * @param string $scope_id
   *   The scope identifier.
   * @param string $key
   *   The memory key.
   * @param mixed $value
   *   The value to store. Must be JSON-serializable and under MAX_VALUE_BYTES.
   * @param int|null $ttl
   *   Time-to-live in seconds, or NULL for no expiry.
   * @param string $backendId
   *   The memory backend plugin ID to use.
   *
   * @throws \Drupal\flowdrop_memory\Exception\MemoryBackendException
   *   If the serialized value exceeds MAX_VALUE_BYTES.
   */
  public function set(string $scope, string $scope_id, string $key, mixed $value, ?int $ttl = NULL, string $backendId = 'static'): void {
    try {
      $serialized = Json::encode($value);
    }
    catch (\Throwable $e) {
      throw new MemoryBackendException(sprintf(
        'Memory value for key "%s" in scope "%s" could not be serialized to JSON: %s',
        $key,
        $scope,
        $e->getMessage(),
      ), 0, $e);
    }
    if (strlen($serialized) > self::MAX_VALUE_BYTES) {
      throw new MemoryBackendException(sprintf(
        'Memory value for key "%s" in scope "%s" exceeds the maximum allowed size of %d bytes (%d bytes given).',
        $key,
        $scope,
        self::MAX_VALUE_BYTES,
        strlen($serialized),
      ));
    }
    $this->getBackend($backendId)->set($scope, $scope_id, $key, $value, $ttl);
  }

  /**
   * Deletes a value from memory.
   *
   * @param string $scope
   *   The memory scope.
   * @param string $scope_id
   *   The scope identifier.
   * @param string $key
   *   The memory key to delete.
   * @param string $backendId
   *   The memory backend plugin ID to use.
   */
  public function delete(string $scope, string $scope_id, string $key, string $backendId = 'static'): void {
    $this->getBackend($backendId)->delete($scope, $scope_id, $key);
  }

  /**
   * Checks whether a key exists in memory.
   *
   * @param string $scope
   *   The memory scope.
   * @param string $scope_id
   *   The scope identifier.
   * @param string $key
   *   The memory key.
   * @param string $backendId
   *   The memory backend plugin ID to use.
   *
   * @return bool
   *   TRUE if the key exists and has not expired.
   */
  public function has(string $scope, string $scope_id, string $key, string $backendId = 'static'): bool {
    return $this->getBackend($backendId)->has($scope, $scope_id, $key);
  }

  /**
   * Lists all keys for a given scope.
   *
   * @param string $scope
   *   The memory scope.
   * @param string $scope_id
   *   The scope identifier.
   * @param string $backendId
   *   The memory backend plugin ID to use.
   *
   * @return array<string>
   *   Array of key names.
   */
  public function keys(string $scope, string $scope_id, string $backendId = 'static'): array {
    return $this->getBackend($backendId)->keys($scope, $scope_id);
  }

  /**
   * Deletes all keys for a given scope.
   *
   * @param string $scope
   *   The memory scope.
   * @param string $scope_id
   *   The scope identifier.
   * @param string $backendId
   *   The memory backend plugin ID to use.
   */
  public function clearScope(string $scope, string $scope_id, string $backendId = 'static'): void {
    $this->getBackend($backendId)->clearScope($scope, $scope_id);
  }

}
