<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media_ui\Service;

use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\StatementInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Pager\PagerManagerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface;
use Drupal\ffmpeg_media\Utility\QueueItemKey;
use Drupal\ffmpeg_media\ValueObject\ConversionProfile;
use Drupal\media\MediaInterface;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Provides data retrieval for the dashboard media listing.
 *
 * @internal
 */
final class MediaListDataService implements MediaListDataServiceInterface {

  use StringTranslationTrait;

  /**
   * Cache tags for dashboard list totals.
   */
  private const array DASHBOARD_TOTAL_CACHE_TAGS = [
    'ffmpeg_media:dashboard',
    'ffmpeg_media:stats',
    'config:ffmpeg_media.settings',
    'config:ffmpeg_media_ui.settings',
  ];

  /**
   * Short-lived cache TTL for access-filtered totals.
   */
  private const int ACCESSIBLE_TOTAL_CACHE_TTL = 10;

  /**
   * Number of items per page for the media listing.
   */
  protected readonly int $itemsPerPage;

  /**
   * Constructs a MediaListDataService object.
   *
   * @param \Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface $formatterSettingsResolver
   *   Formatter settings resolver service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\Core\Database\Connection $database
   *   Database connection.
   * @param \Drupal\Core\Pager\PagerManagerInterface $pagerManager
   *   Pager manager.
   * @param \Symfony\Component\HttpFoundation\RequestStack $requestStack
   *   Request stack.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   *   Module handler service.
   * @param \Drupal\Core\Access\AccessManagerInterface $accessManager
   *   Access manager service.
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
   *   Current user service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   Config factory for reading dashboard_items_per_page.
   * @param \Drupal\Core\Cache\CacheBackendInterface|null $cache
   *   Optional cache backend for short-lived accessible totals.
   */
  public function __construct(
    protected readonly FormatterSettingsResolverInterface $formatterSettingsResolver,
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly Connection $database,
    protected readonly PagerManagerInterface $pagerManager,
    protected readonly RequestStack $requestStack,
    protected readonly ModuleHandlerInterface $moduleHandler,
    protected readonly AccessManagerInterface $accessManager,
    protected readonly AccountProxyInterface $currentUser,
    ConfigFactoryInterface $configFactory,
    protected readonly ?CacheBackendInterface $cache = NULL,
  ) {
    $configured = $configFactory->get('ffmpeg_media_ui.settings')
      ->get('dashboard_items_per_page');
    $this->itemsPerPage = is_numeric($configured) ? (int) $configured : 25;
  }

  /**
   * {@inheritdoc}
   *
   * @phpstan-return array{items: array<int, array<string, mixed>>, total: int}
   */
  #[\Override]
  public function getAllVideoMedia(string $statusFilter): array {
    $contexts = $this->formatterSettingsResolver->getAllContexts();
    if ($contexts === []) {
      return ['items' => [], 'total' => 0];
    }

    $request = $this->requestStack->getCurrentRequest();
    $page = (int) ($request?->query->get('page', 0) ?? 0);
    $offset = $page * $this->itemsPerPage;

    $contextsByKey = [];
    foreach ($contexts as $context) {
      $contextsByKey[$context->bundle . '::' . $context->fieldName] = $context;
    }
    $contextsSignature = $this->buildContextsSignature($contextsByKey);
    $cachedTotal = $this->getCachedAccessibleTotal($statusFilter, $contextsSignature);

    if ($cachedTotal !== NULL && $offset >= $cachedTotal) {
      $this->pagerManager->createPager($cachedTotal, $this->itemsPerPage);
      return ['items' => [], 'total' => $cachedTotal];
    }

    $storage = $this->entityTypeManager->getStorage('media');

    [$baseSql, $arguments] = $this->buildMediaListingBaseQuery($contexts, $statusFilter);
    if ($baseSql === '') {
      $this->pagerManager->createPager(0, $this->itemsPerPage);
      return ['items' => [], 'total' => 0];
    }

    // Raw query used intentionally: the base query is a UNION of per-bundle
    // subqueries that cannot be expressed with the Drupal Select query builder.
    // All user-supplied values are parameterised via $arguments.
    $statement = $this->executeListingQuery($baseSql, $arguments);
    if ($statement === NULL) {
      $this->pagerManager->createPager(0, $this->itemsPerPage);
      return ['items' => [], 'total' => 0];
    }

    $result = $this->buildAccessibleItemsStreaming(
      $storage,
      $statement,
      $contextsByKey,
      $offset,
      $this->itemsPerPage,
      $cachedTotal,
    );

    if ($cachedTotal === NULL) {
      $this->setCachedAccessibleTotal($statusFilter, $contextsSignature, $result['total']);
    }

    $this->pagerManager->createPager($result['total'], $this->itemsPerPage);

    return $result;
  }

  /**
   * {@inheritdoc}
   *
   * @phpstan-param array<int, array<string, mixed>> $items
   * @phpstan-return array<int, array<string, mixed>>
   */
  #[\Override]
  public function prepareMediaTableItems(array $items): array {
    $tableItems = [];
    $bundleIds = [];
    $outputTypesByItemId = $this->getOutputTypesByItemId($items);

    foreach ($items as $item) {
      /** @var \Drupal\media\MediaInterface $media */
      $media = $item['media'];
      $bundleIds[$media->bundle()] = $media->bundle();
    }

    /** @var array<string, \Drupal\media\MediaTypeInterface> $bundleEntities */
    $bundleEntities = $bundleIds !== []
      ? $this->entityTypeManager->getStorage('media_type')->loadMultiple(array_values($bundleIds))
      : [];

    foreach ($items as $item) {
      /** @var \Drupal\media\MediaInterface $media */
      $media = $item['media'];
      /** @var \Drupal\ffmpeg_media\ValueObject\ConversionContext|null $context */
      $context = $item['context'];
      $fieldName = $context?->fieldName ?? '';

      $bundleInfo = $bundleEntities[$media->bundle()] ?? NULL;
      $bundleLabel = $bundleInfo ? $bundleInfo->label() : $media->bundle();
      $fieldLabel = $fieldName !== '' && $media->hasField($fieldName)
        ? (string) $media->get($fieldName)->getFieldDefinition()->getLabel()
        : $fieldName;

      $itemId = (string) ($item['item_id'] ?? QueueItemKey::fromParts((int) $media->id(), $fieldName));
      $outputBadges = $context !== NULL
        ? $this->buildOutputIndicators($outputTypesByItemId[$itemId] ?? [])
        : ['#markup' => ''];

      $tableItems[] = [
        'label' => $media->label(),
        'media_id' => $media->id(),
        'item_id' => $itemId,
        'bundle_label' => $bundleLabel,
        'field_name' => $fieldName,
        'field_label' => $fieldLabel,
        'status' => $item['status'],
        'updated' => $item['updated'],
        'output_badges' => $outputBadges,
        'operations' => $this->buildOperations($media, $fieldName),
      ];
    }

    return $tableItems;
  }

  /**
   * {@inheritdoc}
   *
   * @phpstan-return array<int, array{label: TranslatableMarkup, url: string}>
   */
  #[\Override]
  public function buildQuickActions(): array {
    $actions = [];

    $candidates = [
      [
        'route' => 'ffmpeg_media.settings',
        'label' => new TranslatableMarkup('Settings'),
        'module' => 'ffmpeg_media',
      ],
      [
        'route' => 'ffmpeg_media.profile_list',
        'label' => new TranslatableMarkup('Profiles'),
        'module' => 'ffmpeg_media',
      ],
      [
        'route' => 'ffmpeg_media.cdn_settings',
        'label' => new TranslatableMarkup('CDN'),
        'module' => 'ffmpeg_media',
      ],
      [
        'route' => 'ffmpeg_media_streaming.settings',
        'label' => new TranslatableMarkup('Streaming'),
        'module' => 'ffmpeg_media_streaming',
      ],
      [
        'route' => 'ffmpeg_media_ui.settings',
        'label' => new TranslatableMarkup('UI'),
        'module' => 'ffmpeg_media_ui',
      ],
      [
        'route' => 'ffmpeg_media_whisper.settings',
        'label' => new TranslatableMarkup('Whisper'),
        'module' => 'ffmpeg_media_whisper',
      ],
      [
        'route' => 'ffmpeg_media_image.settings',
        'label' => new TranslatableMarkup('Image'),
        'module' => 'ffmpeg_media_image',
      ],
      [
        'route' => 'ffmpeg_media_player.settings',
        'label' => new TranslatableMarkup('Player'),
        'module' => 'ffmpeg_media_player',
      ],
      [
        'route' => 'ffmpeg_media_advanced.settings',
        'label' => new TranslatableMarkup('Advanced'),
        'module' => 'ffmpeg_media_advanced',
      ],
    ];

    foreach ($candidates as $candidate) {
      if ($this->moduleHandler->moduleExists($candidate['module']) && $this->isRouteAccessible($candidate['route'])) {
        $actions[] = [
          'label' => $candidate['label'],
          'url' => Url::fromRoute($candidate['route'])->toString(),
        ];
      }
    }

    return $actions;
  }

  /**
   * Builds access-aware operations for a media entity.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param string $fieldName
   *   Optional field context.
   *
   * @return array<string, array{title: \Drupal\Core\StringTranslation\TranslatableMarkup, url: \Drupal\Core\Url}>
   *   Operation definitions keyed by machine name.
   */
  #[\NoDiscard]
  protected function buildOperations(MediaInterface $media, string $fieldName = ''): array {
    $operations = [];
    $parameters = ['media' => $media->id()];
    $routeOptions = $fieldName !== '' ? ['query' => ['field_name' => $fieldName]] : [];

    if ($this->isRouteAccessible('entity.media.edit_form', $parameters)) {
      $operations['edit'] = [
        'title' => $this->t('Edit'),
        'url' => Url::fromRoute('entity.media.edit_form', $parameters),
      ];
    }

    if ($this->isRouteAccessible('ffmpeg_media_ui.status', $parameters)) {
      $operations['status'] = [
        'title' => $this->t('Status'),
        'url' => Url::fromRoute('ffmpeg_media_ui.status', $parameters, $routeOptions),
      ];
    }

    if ($this->isRouteAccessible('ffmpeg_media_ui.transcode', $parameters)) {
      $operations['transcode'] = [
        'title' => $this->t('Transcode'),
        'url' => Url::fromRoute('ffmpeg_media_ui.transcode', $parameters, $routeOptions),
      ];
    }

    return $operations;
  }

  /**
   * Checks whether the current user may access a named route.
   *
   * @param string $routeName
   *   The route name to check.
   * @param array<string, mixed> $parameters
   *   Optional route parameters.
   *
   * @return bool
   *   TRUE when access is allowed.
   */
  #[\NoDiscard]
  protected function isRouteAccessible(string $routeName, array $parameters = []): bool {
    return $this->accessManager->checkNamedRoute($routeName, $parameters, $this->currentUser);
  }

  /**
   * Builds the SQL base query for the dashboard media listing.
   *
   * Produces one SELECT per configured conversion context and combines them
   * via UNION ALL so count and page queries can operate directly on field-aware
   * dashboard items instead of scanning all media entities first.
   *
   * @param array<int, \Drupal\ffmpeg_media\ValueObject\ConversionContext> $contexts
   *   Configured conversion contexts.
   * @param string $statusFilter
   *   Status filter value passed by the caller.
   *
   * @return array{0: string, 1: array<string, scalar>}
   *   SQL string and named query arguments.
   */
  #[\NoDiscard]
  protected function buildMediaListingBaseQuery(array $contexts, string $statusFilter): array {
    $queryParts = [];
    $arguments = [];

    foreach (array_values($contexts) as $index => $context) {
      if (!preg_match('/^[A-Za-z0-9_]+$/', $context->fieldName)) {
        continue;
      }

      $fieldTable = 'media__' . $context->fieldName;
      $targetColumn = $context->fieldName . '_target_id';
      $bundleFieldPlaceholder = ':bundle_field_' . $index;
      $bundleMediaPlaceholder = ':bundle_media_' . $index;
      $fieldProgressPlaceholder = ':field_progress_' . $index;
      $fieldOutputPlaceholder = ':field_output_' . $index;
      $fieldValuePlaceholder = ':field_value_' . $index;

      $arguments[$bundleFieldPlaceholder] = $context->bundle;
      $arguments[$bundleMediaPlaceholder] = $context->bundle;
      $arguments[$fieldProgressPlaceholder] = $context->fieldName;
      $arguments[$fieldOutputPlaceholder] = $context->fieldName;
      $arguments[$fieldValuePlaceholder] = $context->fieldName;

      $queryParts[] = sprintf(
        "SELECT DISTINCT mfd.mid AS media_id, %s AS bundle, %s AS field_name, mfd.changed AS changed,
          CASE
            WHEN p.status = 'converting' THEN 'processing'
            WHEN p.status IS NOT NULL THEN p.status
            WHEN o.media_id IS NOT NULL THEN 'completed'
            ELSE 'none'
          END AS item_status,
          CASE
            WHEN p.updated > 0 THEN p.updated
            ELSE NULL
          END AS updated
        FROM {media_field_data} mfd
        INNER JOIN {%s} mf
          ON mf.entity_id = mfd.mid
          AND mf.deleted = 0
          AND mf.bundle = %s
          AND mf.%s IS NOT NULL
        LEFT JOIN {ffmpeg_media_progress} p
          ON p.media_id = mfd.mid
          AND p.field_name = %s
        LEFT JOIN (
          SELECT DISTINCT media_id, field_name
          FROM {ffmpeg_media_outputs}
        ) o
          ON o.media_id = mfd.mid
          AND o.field_name = %s
        WHERE mfd.bundle = %s",
        $bundleFieldPlaceholder,
        $fieldValuePlaceholder,
        $fieldTable,
        $bundleFieldPlaceholder,
        $targetColumn,
        $fieldProgressPlaceholder,
        $fieldOutputPlaceholder,
        $bundleMediaPlaceholder,
      );
    }

    if ($queryParts === []) {
      return ['', []];
    }

    $baseSql = implode(' UNION ALL ', $queryParts);

    if ($statusFilter !== '' && $statusFilter !== 'all') {
      $arguments[':status_filter'] = $statusFilter;
      $baseSql = 'SELECT * FROM (' . $baseSql . ') ffmpeg_media_dashboard_status WHERE item_status = :status_filter';
    }

    return [$baseSql, $arguments];
  }

  /**
   * Executes the dashboard listing query.
   *
   * Returns the raw database statement for streaming — the caller iterates
   * over it in batches instead of materializing all rows at once.
   *
   * @param string $baseSql
   *   The UNION-based listing SQL.
   * @param array<string, scalar> $arguments
   *   Query arguments.
   *
   * @phpstan-return \Drupal\Core\Database\StatementInterface<mixed>|null
   *
   * @return \Drupal\Core\Database\StatementInterface|null
   *   Database statement for streaming iteration, or NULL on failure.
   */
  #[\NoDiscard]
  protected function executeListingQuery(string $baseSql, array $arguments): ?StatementInterface {
    $pageSql = 'SELECT media_id, bundle, field_name, item_status, updated FROM (' . $baseSql . ') ffmpeg_media_dashboard_items ORDER BY changed DESC, media_id DESC';

    return $this->database->query($pageSql, $arguments);
  }

  /**
   * Streams DB rows and builds access-filtered, paginated dashboard items.
   *
   * Reads the database cursor in chunks of 50 rows. Each chunk's media
   * entities are loaded, access-filtered, and released before the next
   * chunk is read, keeping peak memory at O(batch_size) instead of O(n).
   *
   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
   *   The media storage handler.
   * @param \Drupal\Core\Database\StatementInterface<mixed> $statement
   *   Database statement to stream rows from.
   * @param array<string, \Drupal\ffmpeg_media\ValueObject\ConversionContext> $contextsByKey
   *   Conversion contexts keyed by bundle + field.
   * @param int $offset
   *   Number of accessible items to skip (page offset).
   * @param int $limit
   *   Maximum number of items to return for the current page.
   * @param int|null $knownTotal
   *   Cached exact accessible total, if already known.
   *
   * @phpstan-return array{items: array<int, array<string, mixed>>, total: int}
   */
  #[\NoDiscard]
  protected function buildAccessibleItemsStreaming(
    EntityStorageInterface $storage,
    StatementInterface $statement,
    array $contextsByKey,
    int $offset,
    int $limit,
    ?int $knownTotal = NULL,
  ): array {
    $batchSize = 50;
    $accessibleCount = 0;
    $pageItems = [];
    $buffer = [];
    $targetAccessibleCount = $offset + $limit;

    foreach ($statement as $row) {
      $buffer[] = [
        'media_id' => (int) $row->media_id,
        'field_name' => (string) $row->field_name,
        'bundle' => (string) $row->bundle,
        'status' => (string) $row->item_status,
        'updated' => isset($row->updated) ? (int) $row->updated : NULL,
      ];

      if (count($buffer) >= $batchSize) {
        $this->processAccessBatch($storage, $buffer, $contextsByKey, $offset, $limit, $accessibleCount, $pageItems);
        $buffer = [];

        if (
          $knownTotal !== NULL &&
          count($pageItems) >= $limit &&
          $accessibleCount >= $targetAccessibleCount
        ) {
          return ['items' => $pageItems, 'total' => $knownTotal];
        }
      }
    }

    if ($buffer !== []) {
      $this->processAccessBatch($storage, $buffer, $contextsByKey, $offset, $limit, $accessibleCount, $pageItems);
    }

    return ['items' => $pageItems, 'total' => $knownTotal ?? $accessibleCount];
  }

  /**
   * Processes a single batch of rows for access filtering and page collection.
   *
   * @param \Drupal\Core\Entity\EntityStorageInterface $storage
   *   The media storage handler.
   * @param array<int, array{media_id: int, field_name: string, bundle: string, status: string, updated: int|null}> $rows
   *   Batch of normalized rows.
   * @param array<string, \Drupal\ffmpeg_media\ValueObject\ConversionContext> $contextsByKey
   *   Conversion contexts keyed by bundle + field.
   * @param int $offset
   *   Page offset (number of accessible items to skip).
   * @param int $limit
   *   Maximum page items to collect.
   * @param int $accessibleCount
   *   Running count of accessible items (modified in place).
   * @param array<int, array<string, mixed>> $pageItems
   *   Accumulated page items (modified in place).
   */
  private function processAccessBatch(
    EntityStorageInterface $storage,
    array $rows,
    array $contextsByKey,
    int $offset,
    int $limit,
    int &$accessibleCount,
    array &$pageItems,
  ): void {
    $mediaIds = [];
    foreach ($rows as $row) {
      $mediaIds[$row['media_id']] = $row['media_id'];
    }

    /** @var array<int, \Drupal\media\MediaInterface> $mediaEntities */
    $mediaEntities = $mediaIds !== []
      ? $storage->loadMultiple(array_values($mediaIds))
      : [];

    foreach ($rows as $row) {
      $media = $mediaEntities[$row['media_id']] ?? NULL;
      if (!$media instanceof MediaInterface) {
        continue;
      }

      if (!$media->access('view', $this->currentUser)) {
        continue;
      }

      $context = $contextsByKey[$row['bundle'] . '::' . $row['field_name']] ?? NULL;
      if ($context === NULL) {
        continue;
      }

      if ($accessibleCount >= $offset && count($pageItems) < $limit) {
        $pageItems[] = [
          'item_id' => QueueItemKey::fromParts($row['media_id'], $row['field_name']),
          'media' => $media,
          'status' => $row['status'],
          'updated' => $row['updated'],
          'context' => $context,
        ];
      }
      $accessibleCount++;
    }
  }

  /**
   * Gets output type presence keyed by queue item ID for table rows.
   *
   * @param array<int, array<string, mixed>> $items
   *   Prepared dashboard items.
   *
   * @return array<string, array<int, string>>
   *   Output type lists keyed by queue item ID.
   */
  #[\NoDiscard]
  protected function getOutputTypesByItemId(array $items): array {
    $mediaIds = [];

    foreach ($items as $item) {
      $media = $item['media'] ?? NULL;
      if ($media instanceof MediaInterface) {
        $mediaIds[] = (int) $media->id();
      }
    }

    $mediaIds = array_values(array_unique($mediaIds));
    if ($mediaIds === []) {
      return [];
    }

    $result = $this->database->select('ffmpeg_media_outputs', 'o')
      ->fields('o', ['media_id', 'field_name', 'output_type'])
      ->condition('media_id', $mediaIds, 'IN')
      ->distinct()
      ->execute();

    $records = [];
    if ($result !== NULL) {
      foreach ($result as $row) {
        $itemId = QueueItemKey::fromParts((int) $row->media_id, (string) ($row->field_name ?? ''));
        $records[$itemId][] = (string) $row->output_type;
      }
    }

    return $records;
  }

  /**
   * Builds output format indicators for a media entity.
   *
   * @param array<int, string> $outputTypes
   *   Output types present for the current item.
   *
   * @return array<string, mixed>
   *   Render array with output badges.
   */
  #[\NoDiscard]
  protected function buildOutputIndicators(array $outputTypes): array {
    $badges = [];
    foreach ($outputTypes as $type) {
      $label = ConversionProfile::labelFromKey($type);
      $ariaLabel = $this->t('@type output available', ['@type' => $label]);

      $badges[$type] = [
        '#type' => 'html_tag',
        '#tag' => 'span',
        '#value' => $label,
        '#attributes' => [
          'class' => ['ffmpeg-output-badge', 'ffmpeg-output-badge--available'],
          'aria-label' => $ariaLabel,
          'title' => $ariaLabel,
        ],
      ];
    }

    if (empty($badges)) {
      return ['#markup' => ''];
    }

    return [
      '#type' => 'container',
      '#attributes' => ['class' => ['ffmpeg-output-indicators']],
    ] + $badges;
  }

  /**
   * Builds a stable signature for the configured conversion contexts.
   *
   * @param array<string, \Drupal\ffmpeg_media\ValueObject\ConversionContext> $contextsByKey
   *   Conversion contexts keyed by bundle and field name.
   *
   * @return string
   *   Context signature.
   */
  #[\NoDiscard]
  protected function buildContextsSignature(array $contextsByKey): string {
    $keys = array_keys($contextsByKey);
    sort($keys);

    return hash('xxh3', implode('|', $keys));
  }

  /**
   * Loads a cached exact total for the current user and status filter.
   *
   * @param string $statusFilter
   *   Status filter value.
   * @param string $contextsSignature
   *   Signature of the active conversion contexts.
   *
   * @return int|null
   *   Cached total, or NULL when unavailable.
   */
  #[\NoDiscard]
  protected function getCachedAccessibleTotal(string $statusFilter, string $contextsSignature): ?int {
    if ($this->cache === NULL) {
      return NULL;
    }

    $cacheItem = $this->cache->get($this->buildAccessibleTotalCacheId($statusFilter, $contextsSignature));
    if ($cacheItem === FALSE || !is_int($cacheItem->data)) {
      return NULL;
    }

    return $cacheItem->data;
  }

  /**
   * Stores an exact access-filtered total for a short time.
   *
   * @param string $statusFilter
   *   Status filter value.
   * @param string $contextsSignature
   *   Signature of the active conversion contexts.
   * @param int $total
   *   Exact accessible total.
   */
  protected function setCachedAccessibleTotal(string $statusFilter, string $contextsSignature, int $total): void {
    if ($this->cache === NULL) {
      return;
    }

    $this->cache->set(
      $this->buildAccessibleTotalCacheId($statusFilter, $contextsSignature),
      $total,
      time() + self::ACCESSIBLE_TOTAL_CACHE_TTL,
      self::DASHBOARD_TOTAL_CACHE_TAGS,
    );
  }

  /**
   * Builds the cache ID for access-filtered totals.
   *
   * @param string $statusFilter
   *   Status filter value.
   * @param string $contextsSignature
   *   Signature of the active conversion contexts.
   *
   * @return string
   *   Cache ID.
   */
  #[\NoDiscard]
  protected function buildAccessibleTotalCacheId(string $statusFilter, string $contextsSignature): string {
    return 'ffmpeg_media_ui:media_list_total:' . hash(
      'xxh3',
      (string) $this->currentUser->id() . ':' . $statusFilter . ':' . $contextsSignature,
    );
  }

}
