<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Plugin\FFmpegProcessor;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\ffmpeg_media\Attribute\FFmpegProcessor;
use Drupal\ffmpeg_media\Service\FFmpegExecutorInterface;
use Drupal\ffmpeg_media\ValueObject\AudioTrack;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Process\Process;

/**
 * Processor for extracting and encoding multiple audio tracks.
 *
 * This processor extracts audio tracks from a source video for use in
 * multi-language HLS/DASH streaming. Each track is encoded separately
 * and can be referenced in adaptive streaming manifests.
 *
 * Use cases:
 * - Multiple language audio tracks
 * - Audio descriptions for accessibility
 * - Commentary tracks
 * - Different audio mixes (stereo, surround)
 */
#[FFmpegProcessor(
  id: 'multi_audio',
  label: new TranslatableMarkup('Multi-Language Audio'),
  description: new TranslatableMarkup('Extracts and processes multiple audio tracks for HLS/DASH streaming.'),
  outputType: 'audio',
  outputExtension: 'aac',
  requiredFilters: [],
  weight: 15,
)]
final class MultiAudioProcessor extends FFmpegProcessorBase {

  /**
   * Constructs a MultiAudioProcessor object.
   *
   * @param array<string, mixed> $configuration
   *   Plugin configuration.
   * @param string $plugin_id
   *   Plugin ID.
   * @param mixed $plugin_definition
   *   Plugin definition.
   * @param \Drupal\ffmpeg_media\Service\FFmpegExecutorInterface $ffmpegExecutor
   *   FFmpeg executor service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   Config factory service.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    #[Autowire(service: FFmpegExecutorInterface::class)]
    FFmpegExecutorInterface $ffmpegExecutor,
    protected readonly ConfigFactoryInterface $configFactory,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $ffmpegExecutor);
  }

  /**
   * {@inheritdoc}
   *
   * @return array<string, bool|int|string|array<int, array<string, mixed>>>
   *   Default option values.
   */
  #[\Override]
  #[\NoDiscard]
  public function getDefaultOptions(): array {
    $config = $this->configFactory->get('ffmpeg_media.settings');

    return [
      'codec' => $config->get('audio.format.codec') ?? 'libopus',
      'bitrate' => $config->get('audio.format.bitrate') ?? '128k',
      'channels' => 2,
      'sample_rate' => $config->get('audio.format.sample_rate') ?? 48000,
      'normalize' => FALSE,
      'tracks' => [],
    ];
  }

  /**
   * {@inheritdoc}
   *
   * @param string $inputPath
   *   Input file path.
   * @param string $outputPath
   *   Output file path.
   * @param array<string, mixed> $options
   *   Processing options.
   */
  #[\Override]
  #[\NoDiscard]
  public function process(string $inputPath, string $outputPath, array $options = []): bool {
    $options = $this->mergeOptions($options);
    $tracks = $this->buildTracks($options);

    if (empty($tracks)) {
      return FALSE;
    }

    $outputDir = dirname($outputPath);
    $baseName = pathinfo($outputPath, PATHINFO_FILENAME);
    $results = [];

    foreach ($tracks as $track) {
      $trackOutput = $outputDir . DIRECTORY_SEPARATOR . $track->getOutputFilename($baseName);
      $result = $this->processTrack($inputPath, $trackOutput, $track, $options);
      $results[] = $result;

      if (!$result) {
        // Continue processing other tracks even if one fails.
        continue;
      }
    }

    // Return TRUE if at least one track succeeded.
    return in_array(TRUE, $results, TRUE);
  }

  /**
   * Processes a single audio track.
   *
   * @param string $inputPath
   *   Source file path.
   * @param string $outputPath
   *   Output file path.
   * @param \Drupal\ffmpeg_media\ValueObject\AudioTrack $track
   *   Audio track configuration.
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return bool
   *   TRUE if successful.
   */
  protected function processTrack(
    string $inputPath,
    string $outputPath,
    AudioTrack $track,
    array $options,
  ): bool {
    $args = $this->buildTrackArguments($inputPath, $outputPath, $track, $options);
    $command = $this->buildCommandFromArgs($args);

    return $this->ffmpegExecutor->execute($command);
  }

  /**
   * Builds FFmpeg arguments for a single track.
   *
   * @param string $inputPath
   *   Source file path.
   * @param string $outputPath
   *   Output file path.
   * @param \Drupal\ffmpeg_media\ValueObject\AudioTrack $track
   *   Audio track configuration.
   * @param array<string, mixed> $options
   *   Processing options.
   * @param int $outputStreamIndex
   *   Output stream index for metadata tagging (default: 0).
   *
   * @return array<int, string>
   *   FFmpeg arguments.
   */
  protected function buildTrackArguments(
    string $inputPath,
    string $outputPath,
    AudioTrack $track,
    array $options,
    int $outputStreamIndex = 0,
  ): array {
    $args = [
      '-i', $inputPath,
      '-map', sprintf('0:a:%d', $track->streamIndex),
      '-c:a', $track->codec,
      '-b:a', $track->bitrate,
      '-ac', (string) $track->channels,
    ];

    // Sample rate.
    if (isset($options['sample_rate'])) {
      $args[] = '-ar';
      $args[] = (string) $options['sample_rate'];
    }

    // Normalization filter.
    if ($options['normalize'] ?? FALSE) {
      $args[] = '-af';
      $args[] = 'loudnorm=I=-14:TP=-1:LRA=11';
    }

    // Metadata.
    $args[] = sprintf('-metadata:s:a:%d', $outputStreamIndex);
    $args[] = sprintf('language=%s', $track->language);
    $args[] = sprintf('-metadata:s:a:%d', $outputStreamIndex);
    $args[] = sprintf('title=%s', $track->label);

    // Output.
    $args[] = '-y';
    $args[] = $outputPath;

    return $args;
  }

  /**
   * Builds AudioTrack objects from options.
   *
   * @param array<string, mixed> $options
   *   Processing options containing track definitions.
   *
   * @return array<int, AudioTrack>
   *   Array of AudioTrack objects.
   */
  protected function buildTracks(array $options): array {
    $tracks = [];
    $trackData = $options['tracks'] ?? [];

    foreach ($trackData as $data) {
      if ($data instanceof AudioTrack) {
        $tracks[] = $data;
      }
      elseif (is_array($data)) {
        // Apply default codec/bitrate from options.
        $data['codec'] = $data['codec'] ?? $options['codec'];
        $data['bitrate'] = $data['bitrate'] ?? $options['bitrate'];
        $data['channels'] = $data['channels'] ?? $options['channels'];
        $tracks[] = AudioTrack::fromArray($data);
      }
    }

    return $tracks;
  }

  /**
   * Builds FFmpeg command array from arguments.
   *
   * @param array<int, string> $args
   *   FFmpeg arguments.
   *
   * @return array<int, string>
   *   Complete FFmpeg command as array of arguments.
   */
  protected function buildCommandFromArgs(array $args): array {
    $ffmpegPath = $this->ffmpegExecutor->getFfmpegPath();

    return [$ffmpegPath, ...$args];
  }

  /**
   * Detects audio tracks in a source file.
   *
   * @param string $inputPath
   *   Source file path.
   *
   * @return array<int, AudioTrack>
   *   Array of detected audio tracks.
   */
  #[\NoDiscard]
  public function detectTracks(string $inputPath): array {
    $videoInfo = $this->ffmpegExecutor->getVideoInfo($inputPath);

    if ($videoInfo === NULL) {
      return [];
    }

    // Use ffprobe to get detailed audio stream info.
    $streams = $this->probeAudioStreams($inputPath);
    $tracks = [];

    foreach ($streams as $index => $stream) {
      $language = (string) ($stream['language'] ?? 'und');
      $title = (string) ($stream['title'] ?? $this->getLanguageLabel($language));
      $channels = (int) ($stream['channels'] ?? 2);

      $tracks[] = new AudioTrack(
        language: $language,
        label: $title,
        streamIndex: $index,
        isDefault: $index === 0,
        channels: $channels,
      );
    }

    return $tracks;
  }

  /**
   * Probes audio streams using ffprobe.
   *
   * @param string $inputPath
   *   Source file path.
   *
   * @return array<int, array<string, int|string|null>>
   *   Array of audio stream information.
   */
  protected function probeAudioStreams(string $inputPath): array {
    $ffprobePath = $this->ffmpegExecutor->getFfprobePath();
    $command = [
      $ffprobePath,
      '-v', 'quiet',
      '-print_format', 'json',
      '-show_streams',
      '-select_streams', 'a',
      $inputPath,
    ];

    $process = new Process($command);
    $process->setTimeout(60);
    $process->run();

    if (!$process->isSuccessful()) {
      return [];
    }

    $output = $process->getOutput() ?: $process->getErrorOutput();
    if ($output === '') {
      return [];
    }

    $data = json_decode($output, TRUE);

    if (json_last_error() !== JSON_ERROR_NONE || !isset($data['streams'])) {
      return [];
    }

    $streams = [];
    foreach ($data['streams'] as $stream) {
      $streams[] = [
        'language' => $stream['tags']['language'] ?? 'und',
        'title' => $stream['tags']['title'] ?? NULL,
        'channels' => $stream['channels'] ?? 2,
        'codec' => $stream['codec_name'] ?? 'unknown',
        'sample_rate' => $stream['sample_rate'] ?? 48000,
      ];
    }

    return $streams;
  }

  /**
   * Gets a human-readable label for a language code.
   *
   * @param string $code
   *   ISO language code.
   *
   * @return string
   *   Human-readable label.
   */
  protected function getLanguageLabel(string $code): string {
    $labels = [
      'en' => 'English',
      'de' => 'Deutsch',
      'fr' => 'Français',
      'es' => 'Español',
      'it' => 'Italiano',
      'pt' => 'Português',
      'nl' => 'Nederlands',
      'pl' => 'Polski',
      'ru' => 'Русский',
      'ja' => '日本語',
      'ko' => '한국어',
      'zh' => '中文',
      'ar' => 'العربية',
      'hi' => 'हिन्दी',
      'und' => 'Unknown',
    ];

    // Handle regional variants.
    $baseCode = explode('-', $code)[0];

    return $labels[$baseCode] ?? $labels[$code] ?? ucfirst($code);
  }

  /**
   * Generates HLS master playlist entries for audio tracks.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\AudioTrack[] $tracks
   *   Array of audio tracks.
   * @param string $baseUrl
   *   Base URL for audio playlist URIs.
   *
   * @return string
   *   HLS EXT-X-MEDIA entries.
   */
  #[\NoDiscard]
  public function generateHlsPlaylistEntries(array $tracks, string $baseUrl = ''): string {
    $entries = [];

    foreach ($tracks as $track) {
      $uri = $baseUrl . $track->getOutputFilename('audio', 'm3u8');
      $entries[] = $track->toHlsMediaTag($uri);
    }

    return implode("\n", $entries);
  }

  /**
   * {@inheritdoc}
   *
   * @param string $inputPath
   *   Input file path.
   * @param array<string, mixed> $options
   *   Processing options.
   */
  #[\Override]
  #[\NoDiscard]
  public function generateOutputPath(string $inputPath, array $options = []): string {
    $baseName = pathinfo($inputPath, PATHINFO_FILENAME) ?: 'output';
    $directory = pathinfo($inputPath, PATHINFO_DIRNAME) ?: '.';

    return $directory . DIRECTORY_SEPARATOR . $baseName . '_audio';
  }

}
