<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Hook;

use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Template\Attribute;
use Drupal\Core\Url;
use Drupal\ffmpeg_media\Resolver\VideoSourceResolverInterface;
use Drupal\media\MediaInterface;

/**
 * Alters third-party video formatter output to use converted sources.
 *
 * Intercepts the render array of media entities after any formatter has
 * built its output. When converted outputs exist, replaces the original
 * video source URLs with the converted versions (MP4, WebM, etc.).
 *
 * This enables automatic compatibility with third-party video players
 * (Video.js, Plyr, etc.) without requiring those modules to integrate
 * with FFmpeg Media directly.
 *
 * @see \Drupal\ffmpeg_media\Resolver\VideoSourceResolverInterface
 */
final class MediaViewAlterHook {

  /**
   * Formatter IDs handled natively by ffmpeg_media.
   *
   * These formatters already use VideoSourceResolver internally,
   * so we skip them to avoid double-processing.
   *
   * @var array<int, string>
   */
  private const array NATIVE_FORMATTER_IDS = [
    'ffmpeg_media_converted_video',
    'ffmpeg_media_streaming_video',
  ];

  /**
   * Progressive video formats playable by standard HTML5 video elements.
   *
   * Streaming formats (HLS, DASH) require JavaScript players and are
   * excluded from third-party formatter source replacement.
   *
   * @var array<int, string>
   */
  private const array PROGRESSIVE_FORMATS = ['mp4', 'webm', 'mkv', 'flv'];

  /**
   * Constructs a MediaViewAlterHook object.
   *
   * @param \Drupal\ffmpeg_media\Resolver\VideoSourceResolverInterface $videoSourceResolver
   *   The video source resolver service.
   */
  public function __construct(
    private readonly VideoSourceResolverInterface $videoSourceResolver,
  ) {}

  /**
   * Implements hook_media_view_alter().
   *
   * @param array<string, mixed> &$build
   *   The render array for the media entity.
   * @param \Drupal\media\MediaInterface $media
   *   The media entity being rendered.
   * @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
   *   The entity view display.
   */
  #[Hook('media_view_alter')]
  public function mediaViewAlter(array &$build, MediaInterface $media, EntityViewDisplayInterface $display): void {
    $mediaId = (int) $media->id();

    foreach ($display->getComponents() as $fieldName => $component) {
      $formatterId = $component['type'] ?? NULL;

      if ($formatterId === NULL || in_array($formatterId, self::NATIVE_FORMATTER_IDS, TRUE)) {
        continue;
      }

      if (!$media->hasField($fieldName)) {
        continue;
      }

      $fieldType = $media->get($fieldName)->getFieldDefinition()->getType();
      if ($fieldType !== 'file') {
        continue;
      }

      if (!isset($build[$fieldName])) {
        continue;
      }

      // Read format priority from third-party settings if configured,
      // otherwise fall back to progressive formats only (streaming
      // formats require dedicated JS players).
      $thirdParty = $component['third_party_settings']['ffmpeg_media'] ?? [];
      $formatPriority = array_values(array_filter(
        (array) ($thirdParty['format_priority'] ?? self::PROGRESSIVE_FORMATS),
      ));

      $resolved = $this->videoSourceResolver->resolveSources(
        mediaId: $mediaId,
        fieldName: $fieldName,
        formatPriority: $formatPriority,
      );

      if (!$resolved->isConverted) {
        continue;
      }

      $this->replaceSourcesInRenderArray($build[$fieldName], $resolved->sources, $resolved->poster);

      // Propagate resolver cache metadata so Drupal's render cache
      // invalidates when outputs change (conversion, re-conversion, delete).
      $existingTags = $build[$fieldName]['#cache']['tags'] ?? [];
      $build[$fieldName]['#cache']['tags'] = array_values(array_unique(
        array_merge($existingTags, $resolved->cacheTags),
      ));
      $build[$fieldName]['#cache']['contexts'] = array_values(array_unique(
        array_merge($build[$fieldName]['#cache']['contexts'] ?? [], ['user.permissions']),
      ));
    }
  }

  /**
   * Replaces video source URLs in a field render array.
   *
   * Supports multiple render patterns:
   * - Video.js: #theme=videojs with #items as Url objects.
   * - Core/Plyr: #files array (any theme extending FileMediaFormatterBase).
   * - Generic: Any formatter using <source> elements via #items or children.
   *
   * @param array<string, mixed> &$element
   *   The field render array.
   * @param array<int, array{src: string, type: string}> $sources
   *   Converted video sources with src and type.
   * @param string|null $poster
   *   Optional poster image URL.
   */
  private function replaceSourcesInRenderArray(array &$element, array $sources, ?string $poster): void {
    foreach ($element as $delta => &$item) {
      if (!is_int($delta) || !is_array($item)) {
        continue;
      }

      $theme = $item['#theme'] ?? NULL;

      if ($theme === 'videojs') {
        $this->replaceVideoJsSources($item, $sources, $poster);
        continue;
      }

      // Any formatter using #files (core file_video, Plyr, etc.).
      if (isset($item['#files']) && is_array($item['#files'])) {
        $this->replaceFilesSources($item, $sources, $poster);
        continue;
      }

      // Generic fallback: look for nested #items or children with sources.
      $this->replaceGenericSources($item, $sources);
    }
  }

  /**
   * Replaces sources in a Video.js render element.
   *
   * @param array<string, mixed> &$item
   *   The videojs theme render element.
   * @param array<int, array{src: string, type: string}> $sources
   *   Converted video sources.
   * @param string|null $poster
   *   Optional poster image URL.
   */
  private function replaceVideoJsSources(array &$item, array $sources, ?string $poster): void {
    $urls = array_map(
      static function (array $source): Url {
        $src = $source['src'];
        // Absolute URLs (CDN-rewritten) must use fromUri() directly;
        // relative paths use base: prefix for Drupal's URL generation.
        if (str_starts_with($src, 'http://') || str_starts_with($src, 'https://')) {
          return Url::fromUri($src);
        }
        return Url::fromUri('base:' . ltrim($src, '/'));
      },
      $sources,
    );
    $item['#items'] = $urls;

    if ($poster !== NULL) {
      $item['#player_attributes']['poster'] = $poster;
    }
  }

  /**
   * Replaces sources in a render element using the #files pattern.
   *
   * Handles core's file_video theme and any formatter extending
   * FileMediaFormatterBase (e.g., Plyr) that uses #files with
   * source_attributes.
   *
   * @param array<string, mixed> &$item
   *   The render element containing #files.
   * @param array<int, array{src: string, type: string}> $sources
   *   Converted video sources.
   * @param string|null $poster
   *   Optional poster image URL.
   */
  private function replaceFilesSources(array &$item, array $sources, ?string $poster): void {
    $files = [];
    foreach ($sources as $source) {
      $files[] = [
        'source_attributes' => new Attribute([
          'src' => $source['src'],
          'type' => $source['type'],
        ]),
      ];
    }
    $item['#files'] = $files;

    if ($poster !== NULL) {
      $attributes = $item['#attributes'] ?? new Attribute();
      if (!$attributes instanceof Attribute) {
        $attributes = new Attribute((array) $attributes);
      }
      $attributes->setAttribute('poster', $poster);
      $item['#attributes'] = $attributes;
    }
  }

  /**
   * Replaces sources in a generic render element.
   *
   * Falls back to replacing #items if present (array of URLs or strings).
   *
   * @param array<string, mixed> &$item
   *   The render element.
   * @param array<int, array{src: string, type: string}> $sources
   *   Converted video sources.
   */
  private function replaceGenericSources(array &$item, array $sources): void {
    if (!isset($item['#items']) || !is_array($item['#items'])) {
      return;
    }

    $item['#items'] = array_map(
      static fn(array $source): string => $source['src'],
      $sources,
    );
  }

}
