<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\ffmpeg_media\ValueObject\VideoInfo;

/**
 * Service for caching FFprobe results.
 *
 * @internal
 */
final class FFprobeCacheService implements FFprobeCacheServiceInterface {

  /**
   * Cache ID prefix for URI-based cache.
   */
  protected const string CACHE_PREFIX_URI = 'ffprobe:uri:';

  /**
   * Cache ID prefix for hash-based cache.
   */
  protected const string CACHE_PREFIX_HASH = 'ffprobe:hash:';

  /**
   * Default cache TTL in seconds.
   */
  public const int DEFAULT_TTL = FFprobeCacheServiceInterface::DEFAULT_TTL;

  /**
   * Constructs an FFprobeCacheService object.
   *
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   Cache backend.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   Config factory.
   */
  public function __construct(
    protected readonly CacheBackendInterface $cache,
    protected readonly ConfigFactoryInterface $configFactory,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function get(string $uri): ?VideoInfo {
    $cacheId = $this->buildCacheId($uri);
    return $this->getValidCachedVideoInfo(
      $cacheId,
      function () use ($uri): void {
        $this->invalidate($uri);
      },
    );
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function set(string $uri, VideoInfo $info, ?int $ttl = NULL): void {
    $cacheId = $this->buildCacheId($uri);
    $ttl ??= $this->getDefaultTtl();
    $expiry = time() + $ttl;

    $this->cache->set(
      $cacheId,
      $info,
      $expiry,
      ['ffmpeg_media', 'ffprobe_cache'],
    );
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function invalidate(string $uri): void {
    $cacheId = $this->buildCacheId($uri);
    $this->cache->delete($cacheId);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function invalidateAll(): void {
    $this->cache->deleteAll();
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getOrCompute(string $uri, callable $callback): ?VideoInfo {
    // Try to get from cache first.
    $cached = $this->get($uri);
    if ($cached !== NULL) {
      return $cached;
    }

    // Compute the value.
    $info = $callback($uri);

    // Cache if computation was successful.
    if ($info !== NULL) {
      $this->set($uri, $info);
    }

    return $info;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getByHash(string $hash): ?VideoInfo {
    $cacheId = self::CACHE_PREFIX_HASH . $hash;
    return $this->getValidCachedVideoInfo(
      $cacheId,
      function () use ($cacheId): void {
        $this->cache->delete($cacheId);
      },
    );
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function setByHash(string $hash, VideoInfo $info, ?int $ttl = NULL): void {
    $cacheId = self::CACHE_PREFIX_HASH . $hash;
    $ttl ??= $this->getDefaultTtl();
    $expiry = time() + $ttl;

    $this->cache->set(
      $cacheId,
      $info,
      $expiry,
      ['ffmpeg_media', 'ffprobe_cache'],
    );
  }

  /**
   * Builds a cache ID from a file URI.
   *
   * @param string $uri
   *   The file URI.
   *
   * @return string
   *   Cache ID.
   */
  protected function buildCacheId(string $uri): string {
    // Use hash of URI to avoid issues with special characters.
    return self::CACHE_PREFIX_URI . hash('sha256', $uri);
  }

  /**
   * Gets the default TTL configuration.
   *
   * @return int
   *   TTL in seconds.
   */
  protected function getDefaultTtl(): int {
    $config = $this->configFactory->get('ffmpeg_media.settings');
    return (int) ($config->get('ffprobe_cache_ttl') ?? self::DEFAULT_TTL);
  }

  /**
   * Returns a cached VideoInfo object or invalidates stale cache data.
   *
   * @param string $cacheId
   *   Cache ID to inspect.
   * @param callable(): void $invalidate
   *   Invalidator callback when cached data is stale.
   *
   * @return \Drupal\ffmpeg_media\ValueObject\VideoInfo|null
   *   Cached video info or NULL.
   */
  private function getValidCachedVideoInfo(string $cacheId, callable $invalidate): ?VideoInfo {
    $cached = $this->cache->get($cacheId);
    if ($cached === FALSE) {
      return NULL;
    }

    $data = $cached->data;
    if (!$data instanceof VideoInfo) {
      $invalidate();
      return NULL;
    }

    return $data;
  }

}
