<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\ffmpeg_media\Enum\HardwareType;
use Symfony\Component\Process\Process;

/**
 * Service for detecting hardware acceleration capabilities.
 *
 * Integrates with CircuitBreaker to prevent repeated attempts to use
 * failing hardware encoders.
 *
 * @internal
 */
final class HardwareDetector implements HardwareDetectorInterface {

  /**
   * Cache key for hardware detection.
   */
  protected const string CACHE_KEY = 'ffmpeg_media:hardware_detection';

  /**
   * Cached capabilities.
   *
   * @var array{
   *   gpu_name: string|null,
   *   available_encoders: array<int, string>,
   *   available_decoders: array<int, string>,
   *   max_resolution: string|null,
   *   memory: int|null
   * }|null
   */
  protected ?array $capabilities = NULL;

  /**
   * Constructs a HardwareDetector object.
   *
   * @param \Drupal\ffmpeg_media\Service\FFmpegExecutorInterface $ffmpegExecutor
   *   FFmpeg executor service.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   Cache backend.
   * @param \Drupal\ffmpeg_media\Service\CircuitBreakerInterface $circuitBreaker
   *   Circuit breaker service for handling hardware failures.
   * @param \Drupal\ffmpeg_media\Service\HardwareProbeInterface $hardwareProbe
   *   Hardware probe service for system-level GPU detection.
   */
  public function __construct(
    protected readonly FFmpegExecutorInterface $ffmpegExecutor,
    protected readonly CacheBackendInterface $cache,
    protected readonly CircuitBreakerInterface $circuitBreaker,
    protected readonly HardwareProbeInterface $hardwareProbe,
  ) {}

  /**
   * {@inheritdoc}
   *
   * @return array<int, string>
   *   List of available hardware types.
   */
  #[\Override]
  #[\NoDiscard]
  public function detectAvailableHardware(): array {
    $available = [];

    foreach (HardwareType::hardwareTypes() as $hwType) {
      if ($this->checkHardware($hwType)) {
        $available[] = $hwType->value;
      }
    }

    return $available;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function hasHardware(string $hwType): bool {
    return in_array($hwType, $this->detectAvailableHardware(), TRUE);
  }

  /**
   * Records a successful hardware encoding operation.
   *
   * Call this after a hardware encoder successfully completes.
   *
   * @param string $hwType
   *   Hardware type that succeeded.
   */
  public function recordEncodingSuccess(string $hwType): void {
    $this->circuitBreaker->recordSuccess($hwType);
  }

  /**
   * Records a failed hardware encoding operation.
   *
   * Call this after a hardware encoder fails. After multiple failures,
   * the circuit will open and hardware encoding will be skipped.
   *
   * @param string $hwType
   *   Hardware type that failed.
   */
  public function recordEncodingFailure(string $hwType): void {
    $this->circuitBreaker->recordFailure($hwType);
  }

  /**
   * Checks if hardware encoder should be skipped due to circuit breaker.
   *
   * @param string $hwType
   *   Hardware type to check.
   *
   * @return bool
   *   TRUE if hardware should be skipped (circuit is open).
   */
  #[\NoDiscard]
  public function shouldSkipHardware(string $hwType): bool {
    return $this->circuitBreaker->isOpen($hwType);
  }

  /**
   * {@inheritdoc}
   *
   * @return array{
   *   gpu_name: string|null,
   *   available_encoders: array<int, string>,
   *   available_decoders: array<int, string>,
   *   max_resolution: string|null,
   *   memory: int|null
   *   }
   *   Hardware capability details.
   */
  #[\Override]
  #[\NoDiscard]
  public function getCapabilities(): array {
    if ($this->capabilities !== NULL) {
      return $this->capabilities;
    }

    $cached = $this->cache->get(self::CACHE_KEY);
    if ($cached !== FALSE && is_array($cached->data)) {
      $data = $cached->data;
      // Validate cached data structure matches expected type.
      if (array_key_exists('gpu_name', $data)
        && array_key_exists('available_encoders', $data)
        && array_key_exists('available_decoders', $data)
        && array_key_exists('max_resolution', $data)
        && array_key_exists('memory', $data)
        && is_array($data['available_encoders'])
        && is_array($data['available_decoders'])
      ) {
        /** @var array{gpu_name: string|null, available_encoders: array<int, string>, available_decoders: array<int, string>, max_resolution: string|null, memory: int|null} $data */
        $this->capabilities = $data;
        return $this->capabilities;
      }
    }

    $capabilities = [
      'gpu_name' => $this->detectGpuName(),
      'available_encoders' => [],
      'available_decoders' => [],
      'max_resolution' => NULL,
      'memory' => NULL,
    ];

    // Detect available hardware encoders.
    $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',
    ];

    foreach ($hwEncoders as $encoder) {
      if ($this->ffmpegExecutor->hasEncoder($encoder)) {
        $capabilities['available_encoders'][] = $encoder;
      }
    }

    $this->capabilities = $capabilities;
    $this->cache->set(self::CACHE_KEY, $capabilities, time() + 3600);

    return $capabilities;
  }

  /**
   * {@inheritdoc}
   *
   * @return array<int, string>
   *   Hardware acceleration arguments.
   */
  #[\Override]
  #[\NoDiscard]
  public function getHwaccelArguments(string $hwType, string $operation = 'encode'): array {
    $args = [];

    if ($operation !== 'decode') {
      return $args;
    }

    $hw = HardwareType::tryFrom($hwType);
    $args = match ($hw) {
      HardwareType::Nvenc, HardwareType::Cuda => ['-hwaccel', 'cuda', '-hwaccel_output_format', 'cuda'],
      HardwareType::Vaapi => ['-hwaccel', 'vaapi', '-hwaccel_device', '/dev/dri/renderD128'],
      HardwareType::Qsv => ['-hwaccel', 'qsv'],
      HardwareType::VideoToolbox => ['-hwaccel', 'videotoolbox'],
      HardwareType::Vulkan => ['-hwaccel', 'vulkan'],
      HardwareType::Amf => ['-hwaccel', 'd3d11va'],
      HardwareType::OpenHarmony => ['-hwaccel', 'mediacodec'],
      default => [],
    };

    return $args;
  }

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

  /**
   * Checks if a specific hardware type is available.
   *
   * Data-driven approach: verifies system-level hardware presence
   * via HardwareProbe, then checks FFmpeg encoder availability.
   *
   * @param \Drupal\ffmpeg_media\Enum\HardwareType $type
   *   Hardware type to check.
   *
   * @return bool
   *   TRUE if available.
   */
  protected function checkHardware(HardwareType $type): bool {
    return match ($type) {
      HardwareType::Nvenc => $this->hardwareProbe->isNvidiaGpuPresent()
        && $this->ffmpegExecutor->hasEncoder('h264_nvenc'),
      HardwareType::Vaapi => $this->hardwareProbe->isDriDevicePresent()
        && $this->ffmpegExecutor->hasEncoder('h264_vaapi'),
      HardwareType::VideoToolbox => PHP_OS === 'Darwin'
        && $this->ffmpegExecutor->hasEncoder('h264_videotoolbox'),
      HardwareType::Qsv => $this->hardwareProbe->isIntelGpuPresent()
        && $this->ffmpegExecutor->hasEncoder('h264_qsv'),
      HardwareType::Vulkan => $this->hardwareProbe->isAnyGpuPresent()
        && ($this->ffmpegExecutor->hasEncoder('h264_vulkan')
          || $this->ffmpegExecutor->hasEncoder('hevc_vulkan')
          || $this->ffmpegExecutor->hasEncoder('av1_vulkan')),
      HardwareType::Amf => (PHP_OS_FAMILY !== 'Linux' || $this->hardwareProbe->isAmdGpuPresent())
        && $this->ffmpegExecutor->hasEncoder('h264_amf'),
      HardwareType::OpenHarmony => $this->ffmpegExecutor->hasEncoder('h264_mediacodec_ndk'),
      default => FALSE,
    };
  }

  /**
   * Detects GPU name.
   *
   * @return string|null
   *   GPU name or NULL.
   */
  protected function detectGpuName(): ?string {
    // Try nvidia-smi first using array-based Process for security.
    $process = new Process([
      'nvidia-smi',
      '--query-gpu=name',
      '--format=csv,noheader',
    ]);
    $process->setTimeout(5);
    $process->run();

    if ($process->isSuccessful()) {
      $output = trim($process->getOutput());
      if (!empty($output)) {
        return $output;
      }
    }

    // Try lspci on Linux using array-based Process.
    // Note: We cannot pipe directly with array-based Process,
    // so we run lspci and filter in PHP.
    $process = new Process(['lspci']);
    $process->setTimeout(5);
    $process->run();

    if ($process->isSuccessful()) {
      $output = $process->getOutput();
      $lines = explode("\n", $output);

      foreach ($lines as $line) {
        if (stripos($line, 'vga') !== FALSE) {
          if (preg_match('/:\s*(.+)$/', $line, $matches)) {
            return trim($matches[1]);
          }
        }
      }
    }

    return NULL;
  }

}
