<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\ValueObject;

/**
 * Immutable value object representing a video conversion profile.
 *
 * Replaces raw array<string, mixed> profile data throughout ProfileManager
 * and its callers, providing typed access to profile configuration.
 */
final readonly class ConversionProfile {

  /**
   * Constructs a ConversionProfile object.
   *
   * @param string $id
   *   Machine name / profile ID.
   * @param string $label
   *   Human-readable label.
   * @param string $description
   *   Profile description text.
   * @param array<int, \Drupal\ffmpeg_media\ValueObject\ProfileOutput> $outputs
   *   Output configurations.
   * @param bool $generateThumbnail
   *   Whether to generate a thumbnail.
   * @param float $thumbnailTimestamp
   *   Thumbnail capture timestamp in seconds.
   * @param array<int, string> $enabledProcessors
   *   Enabled processor plugin IDs for this profile.
   * @param string|null $hardwareAcceleration
   *   Hardware acceleration requirement ('required', or NULL).
   *
   * @throws \InvalidArgumentException
   *   If id or label is empty.
   */
  public function __construct(
    public string $id,
    public string $label,
    public string $description = '',
    public array $outputs = [],
    public bool $generateThumbnail = TRUE,
    public float $thumbnailTimestamp = 1.0,
    public array $enabledProcessors = [],
    public ?string $hardwareAcceleration = NULL,
  ) {
    if ($id === '') {
      throw new \InvalidArgumentException('Profile ID cannot be empty.');
    }
    if ($label === '') {
      throw new \InvalidArgumentException('Profile label cannot be empty.');
    }
    if ($thumbnailTimestamp < 0) {
      throw new \InvalidArgumentException('Thumbnail timestamp cannot be negative.');
    }
  }

  /**
   * Creates a ConversionProfile from a config array.
   *
   * @param array<string, mixed> $data
   *   Raw profile configuration array (e.g., from config entity).
   *
   * @return self
   *   New ConversionProfile instance.
   */
  #[\NoDiscard]
  public static function fromConfigArray(array $data): self {
    $outputs = [];
    foreach (($data['outputs'] ?? []) as $outputData) {
      if (is_array($outputData)) {
        $outputs[] = ProfileOutput::fromArray($outputData);
      }
    }

    $enabledProcessors = [];
    if (isset($data['enabled_processors']) && is_array($data['enabled_processors'])) {
      foreach ($data['enabled_processors'] as $processor) {
        $enabledProcessors[] = (string) $processor;
      }
    }

    return new self(
      id: (string) ($data['id'] ?? ''),
      label: (string) ($data['label'] ?? ''),
      description: (string) ($data['description'] ?? ''),
      outputs: $outputs,
      generateThumbnail: (bool) ($data['generate_thumbnail'] ?? TRUE),
      thumbnailTimestamp: (float) ($data['thumbnail_timestamp'] ?? 1.0),
      enabledProcessors: $enabledProcessors,
      hardwareAcceleration: isset($data['hardware_acceleration']) ? (string) $data['hardware_acceleration'] : NULL,
    );
  }

  /**
   * Gets enabled output configurations.
   *
   * @return array<int, \Drupal\ffmpeg_media\ValueObject\ProfileOutput>
   *   Only outputs where enabled is TRUE.
   */
  #[\NoDiscard]
  public function getEnabledOutputs(): array {
    return array_values(array_filter(
      $this->outputs,
      static fn(ProfileOutput $output): bool => $output->enabled,
    ));
  }

  /**
   * Gets enabled output container names.
   *
   * @return array<int, string>
   *   Container format names (e.g., ['mp4', 'webm']).
   */
  #[\NoDiscard]
  public function getEnabledOutputContainers(): array {
    return array_values(array_map(
      static fn(ProfileOutput $output): string => $output->container,
      $this->getEnabledOutputs(),
    ));
  }

  /**
   * Gets unique output keys for enabled outputs.
   *
   * When a profile has multiple outputs with the same container (e.g. three
   * MP4 outputs with different codecs), plain container keys would collide.
   * This method returns codec-qualified keys (e.g., 'mp4:hevc', 'mp4:av1')
   * for duplicate containers, and plain keys ('webm') when unique.
   *
   * @return array<int, string>
   *   Output keys (e.g., ['mp4:hevc', 'mp4:h264', 'webm']).
   */
  #[\NoDiscard]
  public function getEnabledOutputKeys(): array {
    $outputs = $this->getEnabledOutputs();

    // Count occurrences of each container.
    $containerCounts = [];
    foreach ($outputs as $output) {
      $containerCounts[$output->container] = ($containerCounts[$output->container] ?? 0) + 1;
    }

    $keys = [];
    foreach ($outputs as $output) {
      if ($containerCounts[$output->container] > 1) {
        $keys[] = $output->container . ':' . $output->codec;
      }
      else {
        $keys[] = $output->container;
      }
    }

    return $keys;
  }

  /**
   * Checks whether a given output key is codec-qualified.
   *
   * @param string $key
   *   Output key (e.g., 'mp4' or 'mp4:hevc').
   *
   * @return bool
   *   TRUE if the key contains a codec qualifier.
   */
  #[\NoDiscard]
  public static function isQualifiedKey(string $key): bool {
    return str_contains($key, ':');
  }

  /**
   * Extracts the container from an output key.
   *
   * @param string $key
   *   Output key (e.g., 'mp4' or 'mp4:hevc').
   *
   * @return string
   *   Container name (e.g., 'mp4').
   */
  #[\NoDiscard]
  public static function containerFromKey(string $key): string {
    return str_contains($key, ':') ? explode(':', $key, 2)[0] : $key;
  }

  /**
   * Formats an output key as a human-readable label.
   *
   * Converts 'mp4:hevc' to 'MP4 (HEVC)', 'mp4' to 'MP4'.
   *
   * @param string $key
   *   Output key.
   *
   * @return string
   *   Human-readable label.
   */
  #[\NoDiscard]
  public static function labelFromKey(string $key): string {
    if (str_contains($key, ':')) {
      [$container, $codec] = explode(':', $key, 2);
      return strtoupper($container) . ' (' . strtoupper($codec) . ')';
    }
    return strtoupper($key);
  }

  /**
   * Finds an enabled output by container format.
   *
   * @param string $container
   *   Container format to find (e.g., 'mp4').
   *
   * @return \Drupal\ffmpeg_media\ValueObject\ProfileOutput|null
   *   The matching output, or NULL if not found or disabled.
   */
  #[\NoDiscard]
  public function findEnabledOutput(string $container): ?ProfileOutput {
    foreach ($this->outputs as $output) {
      if ($output->container === $container && $output->enabled) {
        return $output;
      }
    }
    return NULL;
  }

  /**
   * Whether hardware acceleration is required by this profile.
   *
   * @return bool
   *   TRUE if hardware_acceleration is 'required'.
   */
  #[\NoDiscard]
  public function isHardwareRequired(): bool {
    return $this->hardwareAcceleration === 'required';
  }

  /**
   * Converts to array format compatible with config storage.
   *
   * @return array<string, bool|float|string|array<int, array<string, mixed>>|array<int, string>|null>
   *   Associative array representation.
   */
  #[\NoDiscard]
  public function toArray(): array {
    return [
      'id' => $this->id,
      'label' => $this->label,
      'description' => $this->description,
      'outputs' => array_map(
        static fn(ProfileOutput $output): array => $output->toArray(),
        $this->outputs,
      ),
      'generate_thumbnail' => $this->generateThumbnail,
      'thumbnail_timestamp' => $this->thumbnailTimestamp,
      'enabled_processors' => $this->enabledProcessors,
      'hardware_acceleration' => $this->hardwareAcceleration,
    ];
  }

}
