<?php

declare(strict_types=1);

namespace Drupal\flowdrop_memory\Plugin\MemoryBackend;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\flowdrop_memory\Attribute\MemoryBackend;
use Drupal\flowdrop_memory\Entity\MemoryRecordInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Entity-backed persistent memory backend.
 *
 * Stores values as FlowDrop memory record entities. Values persist across
 * requests and cache clears. Supports optional TTL via the expires field.
 *
 * A request-level read cache minimises entity queries within a single
 * request while keeping the entity as the persistent source of truth.
 *
 * @internal
 */
#[MemoryBackend(
  id: 'entity',
  label: new TranslatableMarkup('Entity (Database)'),
  description: 'Persistent database storage backed by a Drupal content entity. Survives cache clears. Supports TTL via an expires field.',
  persistent: TRUE,
  supports_ttl: TRUE,
)]
class EntityMemoryBackend extends AbstractMemoryBackend {

  /**
   * Request-level read cache: storage_key → decoded value.
   *
   * @var array<string, mixed>
   */
  private array $readCache = [];

  /**
   * {@inheritdoc}
   */
  public function __construct(
    array $configuration,
    string $plugin_id,
    mixed $plugin_definition,
    private readonly EntityTypeManagerInterface $entityTypeManager,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('entity_type.manager'),
    );
  }

  /**
   * {@inheritdoc}
   */
  public function get(string $scope, string $scope_id, string $key, mixed $default = NULL): mixed {
    $cacheKey = $this->buildStorageKey($scope, $scope_id, $key);
    if (array_key_exists($cacheKey, $this->readCache)) {
      return $this->readCache[$cacheKey] ?? $default;
    }

    $record = $this->loadRecord($scope, $scope_id, $key);
    if ($record === NULL || $record->isExpired()) {
      if ($record?->isExpired()) {
        $record->delete();
      }
      $this->readCache[$cacheKey] = NULL;
      return $default;
    }

    $value = $record->getValue();
    $this->readCache[$cacheKey] = $value;
    return $value;
  }

  /**
   * {@inheritdoc}
   */
  public function set(string $scope, string $scope_id, string $key, mixed $value, ?int $ttl = NULL): void {
    $record = $this->loadRecord($scope, $scope_id, $key);
    if ($record === NULL) {
      /** @var \Drupal\flowdrop_memory\Entity\MemoryRecordInterface $record */
      $record = $this->entityTypeManager
        ->getStorage('flowdrop_memory_record')
        ->create([
          'scope'      => $scope,
          'scope_id'   => $scope_id,
          'memory_key' => $key,
        ]);
    }

    $record->setValue($value);
    $record->setExpiry($ttl !== NULL && $ttl > 0 ? time() + $ttl : NULL);
    $record->save();

    // Update read cache.
    $this->readCache[$this->buildStorageKey($scope, $scope_id, $key)] = $value;
  }

  /**
   * {@inheritdoc}
   */
  public function delete(string $scope, string $scope_id, string $key): void {
    $record = $this->loadRecord($scope, $scope_id, $key);
    if ($record !== NULL) {
      $record->delete();
    }
    unset($this->readCache[$this->buildStorageKey($scope, $scope_id, $key)]);
  }

  /**
   * {@inheritdoc}
   */
  public function has(string $scope, string $scope_id, string $key): bool {
    $cacheKey = $this->buildStorageKey($scope, $scope_id, $key);
    if (array_key_exists($cacheKey, $this->readCache)) {
      return $this->readCache[$cacheKey] !== NULL;
    }

    $record = $this->loadRecord($scope, $scope_id, $key);
    if ($record === NULL || $record->isExpired()) {
      return FALSE;
    }
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function keys(string $scope, string $scope_id): array {
    $ids = $this->entityTypeManager
      ->getStorage('flowdrop_memory_record')
      ->getQuery()
      ->accessCheck(FALSE)
      ->condition('scope', $scope)
      ->condition('scope_id', $scope_id)
      ->execute();

    if (empty($ids)) {
      return [];
    }

    /** @var \Drupal\flowdrop_memory\Entity\MemoryRecordInterface[] $records */
    $records = $this->entityTypeManager
      ->getStorage('flowdrop_memory_record')
      ->loadMultiple($ids);

    $keys = [];
    foreach ($records as $record) {
      if (!$record->isExpired()) {
        $keys[] = $record->getMemoryKey();
      }
    }
    return $keys;
  }

  /**
   * {@inheritdoc}
   */
  public function clearScope(string $scope, string $scope_id): void {
    $storage = $this->entityTypeManager->getStorage('flowdrop_memory_record');
    $ids = $storage
      ->getQuery()
      ->accessCheck(FALSE)
      ->condition('scope', $scope)
      ->condition('scope_id', $scope_id)
      ->execute();

    if (!empty($ids)) {
      $records = $storage->loadMultiple($ids);
      $storage->delete($records);
    }

    // Clear read cache entries for this scope.
    $prefix = $this->buildScopePrefix($scope, $scope_id) . ':';
    foreach (array_keys($this->readCache) as $cacheKey) {
      if (str_starts_with($cacheKey, $prefix)) {
        unset($this->readCache[$cacheKey]);
      }
    }
  }

  /**
   * Loads a memory record entity by scope/scope_id/key.
   *
   * @return \Drupal\flowdrop_memory\Entity\MemoryRecordInterface|null
   *   The record, or NULL if not found.
   */
  private function loadRecord(string $scope, string $scope_id, string $key): ?MemoryRecordInterface {
    $ids = $this->entityTypeManager
      ->getStorage('flowdrop_memory_record')
      ->getQuery()
      ->accessCheck(FALSE)
      ->condition('scope', $scope)
      ->condition('scope_id', $scope_id)
      ->condition('memory_key', $key)
      ->range(0, 1)
      ->execute();

    if (empty($ids)) {
      return NULL;
    }

    $record = $this->entityTypeManager
      ->getStorage('flowdrop_memory_record')
      ->load(reset($ids));

    return $record instanceof MemoryRecordInterface ? $record : NULL;
  }

}
