<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Plugin\Field\FieldFormatter;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\ffmpeg_media\Resolver\VideoSourceResolverInterface;
use Drupal\ffmpeg_media\Service\OutputStorageServiceInterface;
use Drupal\ffmpeg_media\Service\ProfileManagerInterface;
use Drupal\ffmpeg_media\ValueObject\ConversionProfile;
use Drupal\ffmpeg_media\Service\ProgressTrackerInterface;
use Drupal\ffmpeg_media_player\Service\PlayerConfigResolverInterface;
use Drupal\media\MediaInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Formatter for displaying converted videos with fallback sources.
 *
 * @extends \Drupal\Core\Field\FormatterBase<\Drupal\file\Plugin\Field\FieldType\FileFieldItemList>
 */
#[FieldFormatter(
  id: 'ffmpeg_media_converted_video',
  label: new TranslatableMarkup('Converted Video'),
  description: new TranslatableMarkup('Plays progressive video files (MP4, WebM). Best for simple single-file playback without adaptive streaming.'),
  field_types: ['file'],
)]
final class ConvertedVideoFormatter extends FormatterBase implements ContainerFactoryPluginInterface {

  /**
   * Extensions playable by the converted video formatter.
   *
   * @var array<int, string>
   */
  private const PLAYABLE_EXTENSIONS = [
    'mp4', 'm4v', 'webm', 'ogv', 'mov', 'mkv', 'flv',
    'avi', 'mpeg', 'mpg', 'wmv', 'ts', '3gp',
  ];

  /**
   * Constructs a ConvertedVideoFormatter object.
   *
   * @param string $plugin_id
   *   Plugin ID.
   * @param mixed $plugin_definition
   *   Plugin definition.
   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
   *   Field definition.
   * @param array<string, mixed> $settings
   *   Formatter settings.
   * @param string $label
   *   Formatter label.
   * @param string $view_mode
   *   View mode.
   * @param array<string, mixed> $third_party_settings
   *   Third party settings.
   * @param \Drupal\ffmpeg_media\Service\OutputStorageServiceInterface $outputStorage
   *   Output storage service.
   * @param \Drupal\ffmpeg_media\Service\ProgressTrackerInterface $progressTracker
   *   Progress tracker service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   Config factory service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   *   Module handler service.
   * @param \Drupal\ffmpeg_media\Service\ProfileManagerInterface $profileManager
   *   Profile manager service.
   * @param \Drupal\ffmpeg_media\Resolver\VideoSourceResolverInterface $videoSourceResolver
   *   Video source resolver service.
   * @param \Drupal\ffmpeg_media_player\Service\PlayerConfigResolverInterface|null $playerConfigResolver
   *   Player config resolver (when ffmpeg_media_player enabled).
   */
  public function __construct(
    $plugin_id,
    $plugin_definition,
    FieldDefinitionInterface $field_definition,
    array $settings,
    $label,
    $view_mode,
    array $third_party_settings,
    #[Autowire(service: OutputStorageServiceInterface::class)]
    protected readonly OutputStorageServiceInterface $outputStorage,
    #[Autowire(service: ProgressTrackerInterface::class)]
    protected readonly ProgressTrackerInterface $progressTracker,
    #[Autowire(service: 'config.factory')]
    protected readonly ConfigFactoryInterface $configFactory,
    #[Autowire(service: 'module_handler')]
    protected readonly ModuleHandlerInterface $moduleHandler,
    #[Autowire(service: ProfileManagerInterface::class)]
    protected readonly ProfileManagerInterface $profileManager,
    #[Autowire(service: VideoSourceResolverInterface::class)]
    protected readonly VideoSourceResolverInterface $videoSourceResolver,
    protected readonly ?PlayerConfigResolverInterface $playerConfigResolver = NULL,
  ) {
    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): static {
    $moduleHandler = $container->get('module_handler');
    $playerConfigResolver = $moduleHandler->moduleExists('ffmpeg_media_player')
      ? $container->get(PlayerConfigResolverInterface::class)
      : NULL;

    return new static(
      $plugin_id,
      $plugin_definition,
      $configuration['field_definition'],
      $configuration['settings'],
      $configuration['label'],
      $configuration['view_mode'],
      $configuration['third_party_settings'],
      $container->get(OutputStorageServiceInterface::class),
      $container->get(ProgressTrackerInterface::class),
      $container->get('config.factory'),
      $moduleHandler,
      $container->get(ProfileManagerInterface::class),
      $container->get(VideoSourceResolverInterface::class),
      $playerConfigResolver,
    );
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public static function isApplicable(FieldDefinitionInterface $field_definition): bool {
    $extensions = $field_definition->getSetting('file_extensions');
    if ($extensions === NULL || $extensions === '') {
      return TRUE;
    }
    $extensionList = array_filter(preg_split('/\s+/', $extensions) ?: []);

    return array_any(
      $extensionList,
      static fn(string $ext): bool => in_array(strtolower($ext), self::PLAYABLE_EXTENSIONS, TRUE),
    );
  }

  /**
   * Supported progressive video output formats and their MIME types.
   */
  private const FORMAT_MIME_TYPES = [
    'mp4' => 'video/mp4',
    'webm' => 'video/webm',
    'mkv' => 'video/x-matroska',
    'flv' => 'video/x-flv',
  ];

  /**
   * Fallback labels when codec info is unavailable.
   *
   * @var array<string, string>
   */
  private const FORMAT_LABELS = [
    'mp4' => 'MP4',
    'webm' => 'WebM',
    'mkv' => 'MKV',
    'flv' => 'FLV',
    'hls' => 'HLS',
    'dash' => 'DASH',
    'jxl' => 'JXL',
  ];

  /**
   * {@inheritdoc}
   *
   * @return array<string, bool|string|array<string>>
   *   Default formatter settings.
   */
  #[\Override]
  public static function defaultSettings(): array {
    return [
      'auto_convert' => TRUE,
      'conversion_profile' => '',
      'controls' => TRUE,
      'autoplay' => FALSE,
      'loop' => FALSE,
      'muted' => FALSE,
      'preload' => 'metadata',
      'show_progress' => TRUE,
      'format_priority' => ['mp4', 'webm', 'mkv', 'flv'],
    ] + parent::defaultSettings();
  }

  /**
   * {@inheritdoc}
   *
   * @param array<string, mixed> $form
   *   Form structure.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   Form state.
   *
   * @return array<string, mixed>
   *   Form elements.
   */
  #[\Override]
  public function settingsForm(array $form, FormStateInterface $form_state): array {
    $elements = parent::settingsForm($form, $form_state);

    $elements['formatter_info'] = [
      '#type' => 'container',
      '#attributes' => ['class' => ['messages', 'messages--info'], 'role' => 'note'],
      'message' => [
        '#markup' => $this->t('Plays converted video files. Uses the Vidstack player when the <em>FFmpeg Media Player</em> submodule is enabled, otherwise falls back to the native HTML5 player. Third-party player modules can integrate via the <code>VideoSourceResolver</code> service.'),
      ],
      '#weight' => -100,
    ];

    $elements['auto_convert'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Auto-convert on upload'),
      '#description' => $this->t('Automatically queue video for conversion when media is created or source file changes.'),
      '#default_value' => $this->getSetting('auto_convert'),
      '#weight' => -20,
    ];

    $profiles = $this->profileManager->getAllProfiles();
    $profileOptions = ['' => $this->t('- Default -')];
    foreach ($profiles as $profile) {
      $profileOptions[$profile->id] = $profile->label;
    }

    $elements['conversion_profile'] = [
      '#type' => 'select',
      '#title' => $this->t('Conversion profile'),
      '#description' => $this->t('Select a conversion profile to use for this field.'),
      '#options' => $profileOptions,
      '#default_value' => $this->getSetting('conversion_profile') ?? '',
      '#weight' => -10,
      '#attributes' => ['data-ffmpeg-profile-select' => TRUE],
    ];

    $elements['controls'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Show controls'),
      '#default_value' => $this->getSetting('controls'),
    ];

    $elements['autoplay'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Autoplay'),
      '#description' => $this->t('Note: Most browsers require muted for autoplay.'),
      '#default_value' => $this->getSetting('autoplay'),
    ];

    $elements['muted'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Muted'),
      '#default_value' => $this->getSetting('muted'),
    ];

    $elements['loop'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Loop'),
      '#default_value' => $this->getSetting('loop'),
    ];

    $elements['preload'] = [
      '#type' => 'select',
      '#title' => $this->t('Preload'),
      '#options' => [
        'none' => $this->t('None'),
        'metadata' => $this->t('Metadata'),
        'auto' => $this->t('Auto'),
      ],
      '#default_value' => $this->getSetting('preload'),
    ];

    $elements['show_progress'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Show conversion progress'),
      '#description' => $this->t('Display progress indicator while video is being converted.'),
      '#default_value' => $this->getSetting('show_progress'),
    ];

    // Resolve the current profile for initial rendering.
    $profileId = $this->getSetting('conversion_profile')
      ?: $this->profileManager->getDefaultProfileId();

    // Build format options for all profiles so JS can swap them.
    $allProfileFormats = [];
    foreach ($profiles as $profile) {
      $allProfileFormats[$profile->id] = $this->buildFormatOptions($profile->id);
    }
    // Map the empty value (- Default -) to the default profile's formats.
    $defaultProfileId = $this->profileManager->getDefaultProfileId();
    $allProfileFormats[''] = $allProfileFormats[$defaultProfileId] ?? [];

    // Build the union of all formats so all checkboxes are pre-rendered.
    $currentProfileFormats = $this->buildFormatOptions($profileId);
    $allFormats = $currentProfileFormats;
    foreach ($allProfileFormats as $profileFormats) {
      foreach ($profileFormats as $key => $label) {
        if (!isset($allFormats[$key])) {
          $allFormats[$key] = $label;
        }
      }
    }

    // Only keep saved selections that still exist in the profile.
    $savedPriority = $this->getSetting('format_priority') ?? [];
    $defaultValue = array_intersect($savedPriority, array_keys($currentProfileFormats));
    // If no saved formats match, enable all by default.
    if ($defaultValue === []) {
      $defaultValue = array_keys($currentProfileFormats);
    }

    $elements['format_priority'] = [
      '#type' => 'checkboxes',
      '#title' => $this->t('Enabled formats'),
      '#description' => $this->t('Available formats are determined by the conversion profile.'),
      '#options' => $allFormats,
      '#default_value' => $defaultValue,
      '#element_validate' => [[$this, 'validateFormatPriority']],
      '#attributes' => ['data-ffmpeg-format-priority' => TRUE],
      '#attached' => [
        'library' => ['ffmpeg_media/formatter_settings'],
        'drupalSettings' => [
          'ffmpegFormatter' => [
            'profiles' => $allProfileFormats,
            'defaultProfile' => $defaultProfileId,
          ],
        ],
      ],
    ];

    // Hide checkboxes not in the current profile via CSS.
    foreach (array_keys($allFormats) as $format) {
      if (!isset($currentProfileFormats[$format])) {
        $elements['format_priority'][$format] = [
          '#wrapper_attributes' => [
            'style' => 'display: none;',
            'data-ffmpeg-hidden' => 'true',
          ],
        ];
      }
    }

    return $elements;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function settingsSummary(): array {
    $summary = [];

    $autoConvert = $this->getSetting('auto_convert');
    $summary[] = $this->t('Auto-convert: @value', [
      '@value' => $autoConvert ? $this->t('Yes') : $this->t('No'),
    ]);

    $profileId = $this->getSetting('conversion_profile') ?: $this->profileManager->getDefaultProfileId();
    $profileData = $this->profileManager->getProfile($profileId);
    $profileLabel = $profileData?->label ?? $profileId;
    $summary[] = $this->t('Profile: @profile', ['@profile' => $profileLabel]);

    $summary[] = $this->t('Features: @features', [
      '@features' => implode(', ', array_filter([
        $this->getSetting('controls') ? (string) $this->t('controls') : NULL,
        $this->getSetting('autoplay') ? (string) $this->t('autoplay') : NULL,
        $this->getSetting('muted') ? (string) $this->t('muted') : NULL,
        $this->getSetting('loop') ? (string) $this->t('loop') : NULL,
      ])) ?: (string) $this->t('none'),
    ]);

    $formats = array_filter(
      $this->getSetting('format_priority') ?? [],
    );
    if ($formats !== []) {
      $formatOptions = $this->buildFormatOptions($profileId);
      $labels = [];
      foreach ($formats as $fmt) {
        $labels[] = $formatOptions[$fmt] ?? strtoupper($fmt);
      }
      $summary[] = $this->t('Formats: @formats', [
        '@formats' => implode(', ', $labels),
      ]);

      $profileKeys = $this->profileManager->getEnabledOutputKeys($profileId);
      $unavailable = array_diff($formats, $profileKeys);
      if ($unavailable !== []) {
        $summary[] = $this->t('Warning: Formats @formats not available in profile', [
          '@formats' => implode(', ', array_map('strtoupper', $unavailable)),
        ]);
      }
    }

    return $summary;
  }

  /**
   * Validates that selected format_priority values exist in the profile.
   *
   * @param array<string, mixed> $element
   *   The form element.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   * @param array<string, mixed> $complete_form
   *   The complete form.
   */
  public function validateFormatPriority(array &$element, FormStateInterface $form_state, array &$complete_form): void {
    $selected = array_filter(
      $form_state->getValue($element['#parents']) ?? [],
    );

    if ($selected === []) {
      return;
    }

    // Find the sibling conversion_profile value.
    $parents = $element['#parents'];
    array_pop($parents);
    $parents[] = 'conversion_profile';
    $profileId = $form_state->getValue($parents) ?: $this->profileManager->getDefaultProfileId();

    $profileKeys = $this->profileManager->getEnabledOutputKeys($profileId);
    $unavailable = array_diff(array_keys($selected), $profileKeys);

    if ($unavailable !== []) {
      $form_state->setError($element, $this->t(
        'The following formats are not available in the selected profile: @formats',
        ['@formats' => implode(', ', $unavailable)],
      ));
    }
  }

  /**
   * Builds format checkbox options from a profile's enabled outputs.
   *
   * Generates labels that include the codec (e.g., "MP4 (AV1)") based
   * on the actual profile configuration rather than hardcoded assumptions.
   *
   * @param string $profileId
   *   The profile ID.
   *
   * @return array<string, string>
   *   Format options keyed by container name.
   */
  #[\NoDiscard]
  protected function buildFormatOptions(string $profileId): array {
    $outputs = $this->profileManager->getEnabledOutputs($profileId);
    $keys = $this->profileManager->getEnabledOutputKeys($profileId);
    $options = [];

    foreach ($outputs as $idx => $output) {
      $key = $keys[$idx] ?? $output->container;
      $label = ConversionProfile::labelFromKey($key);

      // Use the human-readable format label when key is not qualified.
      if (!ConversionProfile::isQualifiedKey($key)) {
        $container = $output->container;
        $codec = strtoupper($output->codec);
        $baseLabel = self::FORMAT_LABELS[$container] ?? strtoupper($container);
        $label = $codec !== '' ? $baseLabel . ' (' . $codec . ')' : $baseLabel;
      }

      $options[$key] = $label;
    }

    return $options;
  }

  /**
   * {@inheritdoc}
   *
   * @param \Drupal\file\Plugin\Field\FieldType\FileFieldItemList $items
   *   Field item list.
   * @param string $langcode
   *   Language code.
   *
   * @return array<int, array<string, mixed>>
   *   Render array elements.
   */
  #[\Override]
  public function viewElements(FieldItemListInterface $items, $langcode): array {
    $elements = [];
    $entity = $items->getEntity();

    if (!$entity instanceof MediaInterface) {
      return $elements;
    }

    $mediaId = (int) $entity->id();
    $fieldName = $this->fieldDefinition->getName();
    $progress = $this->progressTracker->getProgress($mediaId, $fieldName);

    // Resolve converted sources via the shared VideoSourceResolver.
    $enabledFormats = array_filter(
      $this->getSetting('format_priority') ?? [],
    );

    // If still empty, fall back to standard progressive formats.
    if ($enabledFormats === []) {
      $enabledFormats = array_keys(self::FORMAT_MIME_TYPES);
    }

    $profileId = $this->getSetting('conversion_profile') ?: NULL;

    $resolved = $this->videoSourceResolver->resolveSources(
      $mediaId,
      $fieldName,
      $profileId,
      $enabledFormats,
    );

    $sources = $resolved->sources;
    $poster = $resolved->poster;

    // When ffmpeg_media_player is active, use the Vidstack player instead
    // of the native browser <video> element for a consistent, feature-rich
    // playback experience across all video formats.
    if ($this->moduleHandler->moduleExists('ffmpeg_media_player')) {
      $vidstackElements = $this->buildVidstackElement(
        $entity,
        $sources,
        $poster,
        $mediaId,
        $fieldName,
        $progress,
        $resolved->subtitles,
      );
      $vidstackElements[0]['#cache'] = [
        'tags' => [...$resolved->cacheTags, 'config:ffmpeg_media.settings'],
        'contexts' => ['user.permissions'],
        'max-age' => ($progress !== NULL && $progress->isInProgress) ? 0 : -1,
      ];
      return $vidstackElements;
    }

    // Fallback: native HTML5 <video> element.
    $attributes = [];
    if ($this->getSetting('controls')) {
      $attributes['controls'] = TRUE;
    }
    if ($this->getSetting('autoplay')) {
      $attributes['autoplay'] = TRUE;
    }
    if ($this->getSetting('loop')) {
      $attributes['loop'] = TRUE;
    }
    if ($this->getSetting('muted')) {
      $attributes['muted'] = TRUE;
    }
    $attributes['preload'] = $this->getSetting('preload');

    $elements[0] = [
      '#theme' => 'ffmpeg_media_video',
      '#media' => $entity,
      '#sources' => $sources,
      '#poster' => $poster,
      '#attributes' => $attributes,
      '#attached' => [
        'library' => ['ffmpeg_media/progress'],
      ],
      '#cache' => [
        'tags' => [...$resolved->cacheTags, 'config:ffmpeg_media.settings'],
        'contexts' => ['user.permissions'],
        'max-age' => ($progress !== NULL && $progress->isInProgress) ? 0 : -1,
      ],
    ];

    // Add global progress update interval from config.
    $config = $this->configFactory->get('ffmpeg_media.settings');
    $pollInterval = $config->get('progress_update_interval') ?? 2;
    $elements[0]['#attached']['drupalSettings']['ffmpegMedia']['progressUpdateInterval'] = $pollInterval;

    // Add progress indicator if converting.
    if ($this->getSetting('show_progress') && $progress !== NULL && $progress->isInProgress) {
      $elements[0]['#progress'] = $progress->toArray();
      $elements[0]['#attached']['drupalSettings']['ffmpegMedia']['progress'][$mediaId] = [
        'mediaId' => $mediaId,
        'pollInterval' => $pollInterval * 1000,
      ];
    }

    return $elements;
  }

  /**
   * Builds a Vidstack player render element for progressive video.
   *
   * @param \Drupal\media\MediaInterface $entity
   *   The media entity.
   * @param array<int, array{src: string, type: string}> $sources
   *   Video sources array.
   * @param string|null $poster
   *   Poster image URL.
   * @param int $mediaId
   *   Media entity ID.
   * @param string $fieldName
   *   Source field name.
   * @param mixed $progress
   *   Conversion progress data.
   * @param array<int, array{src: string, kind: string, language: string, label: string}> $subtitles
   *   Subtitle/caption tracks.
   *
   * @return array<int, array<string, mixed>>
   *   Render array with Vidstack player element.
   */
  protected function buildVidstackElement(
    MediaInterface $entity,
    array $sources,
    ?string $poster,
    int $mediaId,
    string $fieldName,
    mixed $progress,
    array $subtitles,
  ): array {
    assert($this->playerConfigResolver !== NULL);

    // Use a unique player ID to avoid drupalSettings collision when multiple
    // formatters render the same media entity on one page.
    $playerId = $mediaId . '--' . $this->fieldDefinition->getName();

    $playerConfig = $this->playerConfigResolver->resolve(
      formatterSettings: $this->getSettings(),
      sources: $sources,
      mediaId: $mediaId,
      poster: $poster,
      subtitles: $subtitles,
      title: (string) $entity->label(),
    );
    $playerConfig['playerId'] = $playerId;

    $elements = [];
    $elements[0] = [
      '#theme' => 'ffmpeg_media_streaming_video',
      '#media' => $entity,
      '#sources' => $sources,
      '#poster' => $poster,
      '#subtitles' => $subtitles,
      '#player_config' => $playerConfig,
      '#attached' => [
        'library' => ['ffmpeg_media_player/player'],
        'drupalSettings' => [
          'ffmpegMediaPlayer' => [
            'players' => [
              $playerId => $playerConfig,
            ],
          ],
        ],
      ],
    ];

    if ($this->getSetting('show_progress') && $progress !== NULL && $progress->isInProgress) {
      $elements[0]['#progress'] = $progress->toArray();
    }

    return $elements;
  }

}
