<?php

// phpcs:ignoreFile -- PHP 8.4 property hooks not yet supported by PHPCS.
declare(strict_types=1);

namespace Drupal\ffmpeg_media\ValueObject;

/**
 * Immutable value object representing a subtitle track for streaming output.
 *
 * Subtitle tracks can be included in HLS/DASH adaptive streaming outputs
 * as external WebVTT files with language metadata.
 */
final class SubtitleTrack {

  /**
   * Regex pattern for ISO 639-1 language codes.
   */
  private const string LANGUAGE_PATTERN = '/^[a-z]{2,3}(-[A-Z]{2})?$/';

  /**
   * Constructs a SubtitleTrack object.
   *
   * @param string $language
   *   ISO 639-1 language code (e.g., 'en', 'de', 'es-MX').
   * @param string $label
   *   Human-readable label (e.g., 'English', 'Deutsch').
   * @param string $path
   *   Path to the subtitle file (WebVTT, SRT, or ASS format).
   * @param bool $isDefault
   *   Whether this is the default subtitle track.
   * @param bool $forced
   *   Whether this is a forced subtitle track (always displayed).
   * @param string|null $characteristics
   *   HLS characteristics for accessibility.
   *
   * @throws \InvalidArgumentException
   *   If language code format is invalid or path is empty.
   */
  public function __construct(
    public readonly string $language,
    public readonly string $label,
    public readonly string $path,
    public readonly bool $isDefault = FALSE,
    public readonly bool $forced = FALSE,
    public readonly ?string $characteristics = NULL,
  ) {
    if (!preg_match(self::LANGUAGE_PATTERN, $language)) {
      throw new \InvalidArgumentException(
        sprintf('Invalid language code format: %s. Expected ISO 639-1 format (e.g., "en", "de", "es-MX").', $language)
      );
    }
    if (empty($path)) {
      throw new \InvalidArgumentException('Subtitle track path cannot be empty.');
    }
  }

  /**
   * The unique identifier for this track based on language and forced status.
   */
  public string $id {
    get {
      $id = $this->language;
      if ($this->forced) {
        $id .= '-forced';
      }
      return $id;
    }
  }

  /**
   * Creates SubtitleTrack from configuration array.
   *
   * @param array<string, mixed> $data
   *   Configuration array with language, label, path, is_default, etc.
   *
   * @return self
   *   New SubtitleTrack instance.
   *
   * @throws \InvalidArgumentException
   *   If required fields are missing or invalid.
   */
  #[\NoDiscard]
  public static function fromConfig(array $data): self {
    if (empty($data['language'])) {
      throw new \InvalidArgumentException('Subtitle track requires a language code.');
    }
    if (empty($data['path'])) {
      throw new \InvalidArgumentException('Subtitle track requires a file path.');
    }

    return new self(
      language: (string) $data['language'],
      label: (string) ($data['label'] ?? $data['language']),
      path: (string) $data['path'],
      isDefault: (bool) ($data['is_default'] ?? FALSE),
      forced: (bool) ($data['forced'] ?? FALSE),
      characteristics: $data['characteristics'] ?? NULL,
    );
  }

  /**
   * Creates a default English subtitle track.
   *
   * @param string $path
   *   Path to the subtitle file.
   *
   * @return self
   *   New SubtitleTrack instance.
   */
  #[\NoDiscard]
  public static function english(string $path): self {
    return new self(
      language: 'en',
      label: 'English',
      path: $path,
      isDefault: TRUE,
    );
  }

  /**
   * Creates a closed captions track for accessibility.
   *
   * @param string $language
   *   Language code.
   * @param string $label
   *   Human-readable label.
   * @param string $path
   *   Path to the subtitle file.
   *
   * @return self
   *   New SubtitleTrack instance with accessibility characteristics.
   */
  #[\NoDiscard]
  public static function closedCaptions(string $language, string $label, string $path): self {
    return new self(
      language: $language,
      label: $label,
      path: $path,
      isDefault: FALSE,
      forced: FALSE,
      characteristics: 'public.accessibility.transcribes-spoken-dialog,public.accessibility.describes-music-and-sound',
    );
  }

  /**
   * Gets the subtitle format based on file extension.
   *
   * @return string
   *   Format string: 'webvtt', 'srt', 'ass', or 'unknown'.
   */
  #[\NoDiscard]
  public function getFormat(): string {
    $extension = $this->path
      |> (fn(string $p): string => pathinfo($p, PATHINFO_EXTENSION))
      |> strtolower(...);

    return match ($extension) {
      'vtt', 'webvtt' => 'webvtt',
      'srt' => 'srt',
      'ass', 'ssa' => 'ass',
      default => 'unknown',
    };
  }

  /**
   * Checks if the subtitle file needs conversion to WebVTT.
   *
   * HLS requires WebVTT format for subtitles.
   *
   * @return bool
   *   TRUE if conversion to WebVTT is needed.
   */
  #[\NoDiscard]
  public function needsWebVttConversion(): bool {
    return $this->getFormat() !== 'webvtt';
  }

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

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

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

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

  /**
   * Converts to configuration array.
   *
   * @return array<string, bool|string|null>
   *   Configuration array representation.
   */
  #[\NoDiscard]
  public function toConfig(): array {
    return [
      'language' => $this->language,
      'label' => $this->label,
      'path' => $this->path,
      'is_default' => $this->isDefault,
      'forced' => $this->forced,
      'characteristics' => $this->characteristics,
    ];
  }

  /**
   * Builds HLS EXT-X-MEDIA attributes for this track.
   *
   * @param string $uri
   *   URI to the subtitle playlist.
   *
   * @return string
   *   HLS EXT-X-MEDIA line attributes.
   */
  #[\NoDiscard]
  public function toHlsAttributes(string $uri): string {
    $attrs = [
      'TYPE=SUBTITLES',
      'GROUP-ID="subs"',
      sprintf('NAME="%s"', $this->label),
      sprintf('LANGUAGE="%s"', $this->language),
      sprintf('URI="%s"', $uri),
    ];

    if ($this->isDefault) {
      $attrs[] = 'DEFAULT=YES';
      $attrs[] = 'AUTOSELECT=YES';
    }
    else {
      $attrs[] = 'DEFAULT=NO';
      $attrs[] = 'AUTOSELECT=YES';
    }

    if ($this->forced) {
      $attrs[] = 'FORCED=YES';
    }

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

    return implode(',', $attrs);
  }

}
