<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Component\Plugin\Exception\PluginNotFoundException;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Config\ConfigFactoryInterface;

/**
 * Resolves the preferred audio codec for a given container format.
 *
 * Negotiates between the user's global audio codec preference and the
 * target container's supported codecs. Prioritizes royalty-free codecs
 * when falling back from an incompatible preference.
 *
 * @see \Drupal\ffmpeg_media\Service\AudioCodecResolverInterface
 *
 * @internal
 */
final readonly class AudioCodecResolver implements AudioCodecResolverInterface {

  /**
   * Maps FFmpeg encoder names to human-readable labels.
   *
   * Only codecs in this allowlist are accepted as valid config values.
   *
   * @var array<string, string>
   */
  private const array CODEC_LABELS = [
    'libopus' => 'Opus',
    'aac' => 'AAC',
    'libvorbis' => 'Vorbis',
    'mp3' => 'MP3 (LAME)',
    'flac' => 'FLAC',
    'pcm_s16le' => 'PCM 16-bit',
    'pcm_s24le' => 'PCM 24-bit',
  ];

  /**
   * Maps short config names to canonical FFmpeg encoder names.
   *
   * @var array<string, string>
   */
  private const array CODEC_ALIASES = [
    'opus' => 'libopus',
    'vorbis' => 'libvorbis',
  ];

  /**
   * Ordered list of preferred royalty-free codecs for fallback resolution.
   *
   * @var array<int, string>
   */
  private const array ROYALTY_FREE_PREFERENCE = [
    'libopus',
    'flac',
    'mp3',
    'libvorbis',
  ];

  /**
   * Constructs an AudioCodecResolver.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   * @param \Drupal\Component\Plugin\PluginManagerInterface $outputManager
   *   The output plugin manager for resolving container codec support.
   */
  public function __construct(
    private ConfigFactoryInterface $configFactory,
    private PluginManagerInterface $outputManager,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function resolve(?string $containerId = NULL): string {
    $globalDefault = $this->getGlobalDefault();

    if ($containerId === NULL) {
      return $globalDefault;
    }

    $supported = $this->getSupportedCodecs($containerId);

    // Empty list means all codecs are allowed (e.g., MKV).
    if ($supported === []) {
      return $globalDefault;
    }

    // Preferred codec is compatible with this container.
    if (in_array($globalDefault, $supported, TRUE)) {
      return $globalDefault;
    }

    // Fall back to the first supported royalty-free codec.
    foreach (self::ROYALTY_FREE_PREFERENCE as $codec) {
      if (in_array($codec, $supported, TRUE)) {
        return $codec;
      }
    }

    // Final fallback: first supported codec for this container.
    return $supported[0];
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getGlobalDefault(): string {
    $codec = (string) $this->configFactory
      ->get('ffmpeg_media.settings')
      ->get('audio.format.codec');

    if ($codec === '') {
      return 'libopus';
    }

    // Normalize short names to FFmpeg encoder names.
    $normalized = self::CODEC_ALIASES[$codec] ?? $codec;

    // Validate against allowlist to prevent arbitrary values from config.
    if (!isset(self::CODEC_LABELS[$normalized])) {
      return 'libopus';
    }

    return $normalized;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getCompatibleCodecs(string $containerId): array {
    $supported = $this->getSupportedCodecs($containerId);

    // Empty list means all codecs are allowed.
    if ($supported === []) {
      return self::CODEC_LABELS;
    }

    $filtered = array_filter(
      self::CODEC_LABELS,
      static fn(string $codecId): bool => in_array($codecId, $supported, TRUE),
      ARRAY_FILTER_USE_KEY,
    );

    // Sort the global default to the front if it is compatible.
    $globalDefault = $this->getGlobalDefault();
    if (isset($filtered[$globalDefault])) {
      $preferred = [$globalDefault => $filtered[$globalDefault]];
      unset($filtered[$globalDefault]);
      $filtered = $preferred + $filtered;
    }

    return $filtered;
  }

  /**
   * Gets supported audio codecs for a container from plugin definition.
   *
   * @param string $containerId
   *   Output plugin ID.
   *
   * @return array<int, string>
   *   Supported audio codec IDs, or empty array if all are supported.
   */
  private function getSupportedCodecs(string $containerId): array {
    try {
      $definition = $this->outputManager->getDefinition($containerId);
    }
    catch (PluginNotFoundException) {
      return [];
    }

    $supported = $definition['supportedAudioCodecs'] ?? [];

    return is_array($supported) ? $supported : [];
  }

}
