<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\ffmpeg_media\Enum\HardwareType;
use Drupal\ffmpeg_media\Plugin\FFmpegCodec\FFmpegCodecInterface;
use Drupal\ffmpeg_media\ValueObject\ConversionOptions;
use Drupal\ffmpeg_media\ValueObject\HwAccelOptions;

/**
 * Manages hardware-accelerated encoding selection and pipeline construction.
 *
 * Centralizes encoder selection, hardware pipeline construction, and
 * hardware detection logic previously spread across VideoConverter.
 *
 * @internal
 */
final class HardwareAccelerationManager implements HardwareAccelerationManagerInterface {

  /**
   * Hardware acceleration mode: auto-detect best encoder.
   */
  private const string HW_AUTO = 'auto';

  /**
   * Hardware acceleration mode: disabled (software only).
   */
  private const string HW_DISABLED = 'disabled';

  /**
   * Cache key for persistent encoder availability data.
   */
  private const string ENCODER_CACHE_KEY = 'ffmpeg_media:encoders';

  /**
   * Persistent encoder cache TTL in seconds (1 hour).
   */
  private const int ENCODER_CACHE_TTL = 3600;

  /**
   * In-memory cache of encoder availability checks.
   *
   * Populated from the persistent Drupal cache on first access. New
   * lookups are written back to the persistent cache so subsequent
   * requests avoid repeated subprocess calls to `ffmpeg -encoders`.
   *
   * @var array<string, bool>|null
   */
  private ?array $encoderCache = NULL;

  /**
   * Constructs a HardwareAccelerationManager.
   *
   * @param \Drupal\ffmpeg_media\Service\HardwareDetectorInterface $hardwareDetector
   *   Hardware detector service.
   * @param \Drupal\ffmpeg_media\Service\FFmpegExecutorInterface $ffmpegExecutor
   *   FFmpeg executor service.
   * @param \Drupal\ffmpeg_media\Service\ConfigValueProviderInterface $configValueProvider
   *   Config value provider for cached config reads.
   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
   *   Logger channel.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   Cache backend for persistent encoder availability data.
   * @param \Drupal\ffmpeg_media\Service\HardwarePipelineInterface|null $hardwarePipeline
   *   Hardware pipeline service for GPU encoding.
   */
  public function __construct(
    protected readonly HardwareDetectorInterface $hardwareDetector,
    protected readonly FFmpegExecutorInterface $ffmpegExecutor,
    protected readonly ConfigValueProviderInterface $configValueProvider,
    protected readonly LoggerChannelInterface $logger,
    protected readonly CacheBackendInterface $cache,
    protected readonly ?HardwarePipelineInterface $hardwarePipeline = NULL,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function selectEncoder(
    FFmpegCodecInterface $codecPlugin,
    string $codecId,
  ): array {
    $hwAccel = $this->configValueProvider->getHardwareAcceleration();

    if ($hwAccel === self::HW_DISABLED) {
      return ['encoder' => NULL, 'hwType' => NULL];
    }

    if ($hwAccel === self::HW_AUTO) {
      // Auto-detect: iterate available hardware in preference order.
      $available = $this->hardwareDetector->detectAvailableHardware();

      foreach (HardwareType::preferenceOrder() as $hwType) {
        if (!in_array($hwType->value, $available, TRUE)) {
          continue;
        }

        if ($this->hardwareDetector->shouldSkipHardware($hwType->value)) {
          continue;
        }

        $encoder = $hwType->encoderForCodec($codecId);
        if ($encoder !== NULL && $this->hasEncoderCached($encoder)) {
          return ['encoder' => $encoder, 'hwType' => $hwType];
        }
      }

      return ['encoder' => NULL, 'hwType' => NULL];
    }

    // Specific hardware type requested.
    $hwType = HardwareType::tryFrom($hwAccel);
    if ($hwType !== NULL) {
      $encoder = $hwType->encoderForCodec($codecId);
      if ($encoder !== NULL && $this->hasEncoderCached($encoder)) {
        return ['encoder' => $encoder, 'hwType' => $hwType];
      }
    }

    $this->logger->warning(
      'Requested hardware acceleration @hw not available for codec @codec, using software encoder.',
      ['@hw' => $hwAccel, '@codec' => $codecId],
    );

    return ['encoder' => NULL, 'hwType' => NULL];
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function buildHardwarePipeline(
    string $codec,
    ConversionOptions $options,
  ): ?array {
    if ($this->hardwarePipeline === NULL) {
      $this->logger->debug('Hardware pipeline not available: HardwarePipelineInterface not injected. GPU pipeline construction skipped.');
      return NULL;
    }

    $hwMode = $this->configValueProvider->getHardwareAcceleration();

    if ($hwMode === self::HW_DISABLED) {
      return NULL;
    }

    // Detect best hardware and create HwAccelOptions.
    $hwAccel = $this->detectBestHwAccelOptions($codec);
    if ($hwAccel === NULL) {
      return NULL;
    }

    $pipeline = $this->hardwarePipeline->buildPipeline($codec, $options, $hwAccel);

    if ($pipeline['encoder'] === NULL) {
      return NULL;
    }

    return $pipeline;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function extractHwTypeFromPipeline(array $pipeline): ?HardwareType {
    $encoder = $pipeline['encoder'];
    if ($encoder === NULL) {
      return NULL;
    }

    return HardwareType::fromEncoderSuffix($encoder);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function detectBestHardware(): ?string {
    $hwMode = $this->configValueProvider->getHardwareAcceleration();

    if ($hwMode === self::HW_DISABLED) {
      return NULL;
    }

    $available = $this->hardwareDetector->detectAvailableHardware();

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

    $preference = array_map(
      static fn(HardwareType $t): string => $t->value,
      HardwareType::preferenceOrder(),
    );

    $preferred = array_find(
      $preference,
      static fn(string $hw): bool => in_array($hw, $available, TRUE),
    );
    if ($preferred !== NULL) {
      return $preferred;
    }

    return array_first($available);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function overrideEncoder(array $args, string $encoder): array {
    $index = array_find_key(
      $args,
      static fn(string $arg): bool => $arg === '-c:v',
    );
    if ($index !== NULL && isset($args[$index + 1])) {
      $args[$index + 1] = $encoder;
    }
    return $args;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function clearEncoderCache(): void {
    $this->encoderCache = NULL;
    $this->cache->delete(self::ENCODER_CACHE_KEY);
  }

  /**
   * {@inheritdoc}
   *
   * @phpstan-return int
   */
  #[\Override]
  public function warmEncoderCache(): int {
    // All known hardware encoders to probe.
    $hwEncoders = [
      'h264_nvenc', 'hevc_nvenc', 'av1_nvenc',
      'h264_vaapi', 'hevc_vaapi', 'av1_vaapi', 'vp9_vaapi',
      'h264_qsv', 'hevc_qsv', 'av1_qsv', 'vp9_qsv',
      'h264_videotoolbox', 'hevc_videotoolbox',
      'h264_amf', 'hevc_amf', 'av1_amf',
      'h264_vulkan', 'hevc_vulkan', 'av1_vulkan',
      'h264_mediacodec_ndk', 'hevc_mediacodec_ndk',
    ];

    // Clear any stale in-memory state so loadEncoderCache() re-reads.
    $this->encoderCache = [];

    $availableCount = 0;
    foreach ($hwEncoders as $encoder) {
      $available = $this->ffmpegExecutor->hasEncoder($encoder);
      $this->encoderCache[$encoder] = $available;
      if ($available) {
        $availableCount++;
      }
    }

    $this->persistEncoderCache();

    $this->logger->info('Encoder cache warmed: @available of @total hardware encoders available.', [
      '@available' => $availableCount,
      '@total' => count($hwEncoders),
    ]);

    return $availableCount;
  }

  /**
   * Checks encoder availability with persistent caching.
   *
   * Wraps FFmpegExecutor::hasEncoder() with a two-level cache: an
   * in-memory array for the current request and a Drupal cache backend
   * with a 1-hour TTL across requests. This avoids repeated subprocess
   * calls to `ffmpeg -encoders`.
   *
   * @param string $encoder
   *   Encoder name (e.g., 'h264_nvenc', 'hevc_vaapi').
   *
   * @return bool
   *   TRUE if the encoder is available.
   */
  private function hasEncoderCached(string $encoder): bool {
    $this->loadEncoderCache();
    if ($this->encoderCache === NULL) {
      $this->encoderCache = [];
    }

    if (array_key_exists($encoder, $this->encoderCache)) {
      return $this->encoderCache[$encoder];
    }

    $available = $this->ffmpegExecutor->hasEncoder($encoder);
    $this->encoderCache[$encoder] = $available;
    $this->persistEncoderCache();

    return $available;
  }

  /**
   * Loads encoder cache from persistent storage into memory.
   */
  private function loadEncoderCache(): void {
    if ($this->encoderCache !== NULL) {
      return;
    }

    $cached = $this->cache->get(self::ENCODER_CACHE_KEY);
    $this->encoderCache = ($cached !== FALSE && is_array($cached->data))
      ? $cached->data
      : [];
  }

  /**
   * Writes the in-memory encoder cache to persistent storage.
   */
  private function persistEncoderCache(): void {
    if ($this->encoderCache === NULL) {
      return;
    }

    $this->cache->set(
      self::ENCODER_CACHE_KEY,
      $this->encoderCache,
      time() + self::ENCODER_CACHE_TTL,
      ['ffmpeg_media'],
    );
  }

  /**
   * Detects the best hardware and returns HwAccelOptions for a codec.
   *
   * Consolidates detection logic previously split across HardwareDetector
   * and HardwarePipeline into a single method.
   *
   * @param string $codec
   *   Target codec (h264, hevc, av1, vp9).
   *
   * @return \Drupal\ffmpeg_media\ValueObject\HwAccelOptions|null
   *   HwAccelOptions for the best available hardware, or NULL.
   */
  protected function detectBestHwAccelOptions(string $codec): ?HwAccelOptions {
    $bestHw = $this->detectBestHardware();
    if ($bestHw === NULL) {
      return NULL;
    }

    $hwType = HardwareType::tryFrom($bestHw);
    if ($hwType === NULL) {
      return NULL;
    }

    // Verify this hardware supports the requested codec.
    $encoder = $hwType->encoderForCodec($codec);
    if ($encoder === NULL || !$this->hasEncoderCached($encoder)) {
      return NULL;
    }

    return $this->createHwAccelForType($hwType);
  }

  /**
   * Creates HwAccelOptions for a hardware type.
   *
   * @param \Drupal\ffmpeg_media\Enum\HardwareType $hwType
   *   Hardware type enum value.
   *
   * @return \Drupal\ffmpeg_media\ValueObject\HwAccelOptions|null
   *   HwAccelOptions instance or NULL if not supported.
   */
  protected function createHwAccelForType(HardwareType $hwType): ?HwAccelOptions {
    return match ($hwType) {
      HardwareType::Nvenc => HwAccelOptions::forNvenc(),
      HardwareType::Vaapi => HwAccelOptions::forVaapi(),
      HardwareType::Qsv => HwAccelOptions::forQsv(),
      HardwareType::VideoToolbox => HwAccelOptions::forVideoToolbox(),
      HardwareType::Amf => HwAccelOptions::forAmf(),
      HardwareType::Vulkan => HwAccelOptions::forVulkan(),
      default => NULL,
    };
  }

}
