<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\ValueObject;

/**
 * Immutable value object representing an audio track for HLS/DASH streaming.
 *
 * Audio tracks can be used for:
 * - Multiple language versions of content
 * - Audio descriptions for accessibility
 * - Director's commentary
 * - Different audio mixes (stereo, 5.1, Atmos)
 */
final readonly class AudioTrack {

  /**
   * Characteristics constants for accessibility.
   */
  public const string CHARACTERISTIC_DESCRIBES_VIDEO = 'public.accessibility.describes-video';
  public const string CHARACTERISTIC_TRANSCRIBES_DIALOG = 'public.accessibility.transcribes-spoken-dialog';

  /**
   * Constructs an AudioTrack object.
   *
   * @param string $language
   *   ISO 639-1 or ISO 639-2 language code (e.g., 'en', 'de', 'en-US').
   * @param string $label
   *   Human-readable label for the track (e.g., 'English', 'Deutsch').
   * @param int $streamIndex
   *   Audio stream index in source file (0-based).
   * @param bool $isDefault
   *   Whether this is the default audio track.
   * @param string $codec
   *   Audio codec for output (aac, libopus, etc.).
   * @param string $bitrate
   *   Audio bitrate (e.g., '128k', '192k').
   * @param int $channels
   *   Number of audio channels.
   * @param string|null $characteristics
   *   HLS characteristics for accessibility features.
   * @param string|null $groupId
   *   Audio group ID for HLS (defaults to 'audio').
   * @param bool $autoSelect
   *   Whether player should auto-select this track.
   * @param string|null $outputPath
   *   Path to extracted/encoded audio file.
   *
   * @throws \InvalidArgumentException
   *   If language code format is invalid.
   */
  public string $label;

  public function __construct(
    public string $language,
    string $label,
    public int $streamIndex = 0,
    public bool $isDefault = FALSE,
    public string $codec = 'libopus',
    public string $bitrate = '128k',
    public int $channels = 2,
    public ?string $characteristics = NULL,
    public ?string $groupId = 'audio',
    public bool $autoSelect = TRUE,
    public ?string $outputPath = NULL,
  ) {
    // Strip null bytes and control characters from label.
    $this->label = (string) preg_replace('/[\x00-\x1F\x7F]/', '', $label);
    if (!self::isValidLanguageCode($language)) {
      throw new \InvalidArgumentException(
        sprintf('Invalid language code format: %s. Expected ISO 639-1 (xx) or ISO 639-2 (xxx) with optional region (xx-XX).', $language)
      );
    }
    if ($streamIndex < 0) {
      throw new \InvalidArgumentException('Stream index must be 0 or positive.');
    }
    if ($channels < 1 || $channels > 8) {
      throw new \InvalidArgumentException('Channels must be between 1 and 8.');
    }
  }

  /**
   * Validates a language code format.
   *
   * @param string $code
   *   Language code to validate.
   *
   * @return bool
   *   TRUE if valid.
   */
  public static function isValidLanguageCode(string $code): bool {
    // ISO 639-1 (2 chars), ISO 639-2 (3 chars), optionally with region.
    return (bool) preg_match('/^[a-z]{2,3}(-[A-Z]{2})?$/', $code);
  }

  /**
   * Creates AudioTrack from an array.
   *
   * @param array<string, mixed> $data
   *   Associative array of settings.
   *
   * @return self
   *   New AudioTrack instance.
   */
  #[\NoDiscard]
  public static function fromArray(array $data): self {
    return new self(
      language: (string) ($data['language'] ?? 'und'),
      label: (string) ($data['label'] ?? $data['language'] ?? 'Unknown'),
      streamIndex: (int) ($data['stream_index'] ?? 0),
      isDefault: (bool) ($data['is_default'] ?? FALSE),
      codec: (string) ($data['codec'] ?? 'libopus'),
      bitrate: (string) ($data['bitrate'] ?? '128k'),
      channels: (int) ($data['channels'] ?? 2),
      characteristics: isset($data['characteristics']) ? (string) $data['characteristics'] : NULL,
      groupId: isset($data['group_id']) ? (string) $data['group_id'] : 'audio',
      autoSelect: (bool) ($data['auto_select'] ?? TRUE),
      outputPath: isset($data['output_path']) ? (string) $data['output_path'] : NULL,
    );
  }

  /**
   * Creates a track for audio description (accessibility).
   *
   * @param string $language
   *   Language code.
   * @param int $streamIndex
   *   Stream index in source.
   *
   * @return self
   *   Track configured for audio description.
   */
  #[\NoDiscard]
  public static function forAudioDescription(string $language, int $streamIndex = 0): self {
    return new self(
      language: $language,
      label: 'Audio Description',
      streamIndex: $streamIndex,
      isDefault: FALSE,
      characteristics: self::CHARACTERISTIC_DESCRIBES_VIDEO,
      autoSelect: FALSE,
    );
  }

  /**
   * Creates a track for commentary.
   *
   * @param string $language
   *   Language code.
   * @param string $label
   *   Commentary label (e.g., "Director's Commentary").
   * @param int $streamIndex
   *   Stream index in source.
   *
   * @return self
   *   Track configured for commentary.
   */
  #[\NoDiscard]
  public static function forCommentary(string $language, string $label, int $streamIndex = 0): self {
    return new self(
      language: $language,
      label: $label,
      streamIndex: $streamIndex,
      isDefault: FALSE,
      autoSelect: FALSE,
    );
  }

  /**
   * Creates a new instance with different output path.
   *
   * @param string $outputPath
   *   Output file path.
   *
   * @return self
   *   New instance with output path.
   */
  #[\NoDiscard]
  public function withOutputPath(string $outputPath): self {
    return clone($this, ['outputPath' => $outputPath]);
  }

  /**
   * Creates a new instance marked as default.
   *
   * @return self
   *   New instance marked as default.
   */
  #[\NoDiscard]
  public function asDefault(): self {
    return clone($this, ['isDefault' => TRUE]);
  }

  /**
   * Creates a new instance with different codec settings.
   *
   * @param string $codec
   *   Audio codec.
   * @param string $bitrate
   *   Audio bitrate.
   * @param int $channels
   *   Number of channels.
   *
   * @return self
   *   New instance with updated codec settings.
   */
  #[\NoDiscard]
  public function withCodec(string $codec, string $bitrate = '128k', int $channels = 2): self {
    return clone($this, [
      'codec' => $codec,
      'bitrate' => $bitrate,
      'channels' => $channels,
    ]);
  }

  /**
   * Generates HLS EXT-X-MEDIA tag for this track.
   *
   * @param string $uri
   *   URI to the audio playlist/segment.
   *
   * @return string
   *   Complete EXT-X-MEDIA tag.
   */
  #[\NoDiscard]
  public function toHlsMediaTag(string $uri): string {
    $attrs = [
      'TYPE=AUDIO',
      sprintf('GROUP-ID="%s"', $this->groupId ?? 'audio'),
      sprintf('NAME="%s"', $this->escapeHlsString($this->label)),
      sprintf('LANGUAGE="%s"', $this->language),
      sprintf('DEFAULT=%s', $this->isDefault ? 'YES' : 'NO'),
      sprintf('AUTOSELECT=%s', $this->autoSelect ? 'YES' : 'NO'),
    ];

    if ($this->characteristics !== NULL) {
      $attrs[] = sprintf('CHARACTERISTICS="%s"', $this->characteristics);
    }

    if ($this->channels > 2) {
      $attrs[] = sprintf('CHANNELS="%d"', $this->channels);
    }

    $attrs[] = sprintf('URI="%s"', $uri);

    return '#EXT-X-MEDIA:' . implode(',', $attrs);
  }

  /**
   * Generates DASH AdaptationSet attributes for this track.
   *
   * @return array<string, string>
   *   Array of XML attributes for AdaptationSet.
   */
  #[\NoDiscard]
  public function toDashAdaptationSetAttrs(): array {
    $attrs = [
      'contentType' => 'audio',
      'lang' => $this->language,
      'label' => $this->label,
    ];

    if ($this->isDefault) {
      $attrs['default'] = 'true';
    }

    return $attrs;
  }

  /**
   * Builds FFmpeg arguments for extracting this audio track.
   *
   * @param string $inputPath
   *   Source file path.
   * @param string $outputPath
   *   Output file path.
   * @param int $outputStreamIndex
   *   Output stream index for metadata tagging (default: 0).
   *
   * @return array<int, string>
   *   FFmpeg command arguments.
   */
  #[\NoDiscard]
  public function buildExtractArguments(string $inputPath, string $outputPath, int $outputStreamIndex = 0): array {
    $args = [
      '-i', $inputPath,
      '-map', sprintf('0:a:%d', $this->streamIndex),
      '-c:a', $this->codec,
      '-b:a', $this->bitrate,
      '-ac', (string) $this->channels,
    ];

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

    $args[] = '-y';
    $args[] = $outputPath;

    return $args;
  }

  /**
   * Gets the expected output filename for this track.
   *
   * @param string $baseName
   *   Base filename without extension.
   * @param string $extension
   *   File extension (default: based on codec).
   *
   * @return string
   *   Output filename.
   */
  #[\NoDiscard]
  public function getOutputFilename(string $baseName, ?string $extension = NULL): string {
    $ext = $extension ?? $this->getExtensionForCodec();
    return sprintf('%s_audio_%s.%s', $baseName, $this->language, $ext);
  }

  /**
   * Gets the file extension for the current codec.
   *
   * @return string
   *   File extension without dot.
   */
  #[\NoDiscard]
  public function getExtensionForCodec(): string {
    return match ($this->codec) {
      'libopus' => 'opus',
      'libvorbis' => 'ogg',
      'flac' => 'flac',
      'mp3', 'libmp3lame' => 'mp3',
      'ac3', 'eac3' => 'ac3',
      'aac' => 'aac',
      default => $this->codec,
    };
  }

  /**
   * Escapes a string for HLS playlist.
   *
   * @param string $value
   *   String to escape.
   *
   * @return string
   *   Escaped string.
   */
  protected function escapeHlsString(string $value): string {
    return str_replace(['"', "\n", "\r"], ['\"', '', ''], $value);
  }

  /**
   * Converts to array format.
   *
   * @return array<string, bool|int|string|null>
   *   Associative array of settings.
   */
  #[\NoDiscard]
  public function toArray(): array {
    return [
      'language' => $this->language,
      'label' => $this->label,
      'stream_index' => $this->streamIndex,
      'is_default' => $this->isDefault,
      'codec' => $this->codec,
      'bitrate' => $this->bitrate,
      'channels' => $this->channels,
      'characteristics' => $this->characteristics,
      'group_id' => $this->groupId,
      'auto_select' => $this->autoSelect,
      'output_path' => $this->outputPath,
    ];
  }

}
