<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media_player\Plugin\Field\FieldFormatter;

use Drupal\Core\Config\ConfigFactoryInterface;
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\Service\ProgressTrackerInterface;
use Drupal\ffmpeg_media\ValueObject\ConversionProfile;
use Drupal\ffmpeg_media_player\Service\PlayerConfigResolverInterface;
use Drupal\media\MediaInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Formatter for video playback via Vidstack (HLS, DASH, MP4, WebM).
 *
 * @extends \Drupal\Core\Field\FormatterBase<\Drupal\file\Plugin\Field\FieldType\FileFieldItemList>
 */
#[FieldFormatter(
  id: 'ffmpeg_media_streaming_video',
  label: new TranslatableMarkup('Video Player (Vidstack)'),
  description: new TranslatableMarkup('Full-featured player with adaptive streaming (HLS, DASH) and quality switching. Use this when your conversion profile includes streaming outputs.'),
  field_types: ['file'],
)]
final class VideoPlayerFormatter extends FormatterBase implements ContainerFactoryPluginInterface {

  /**
   * 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',
  ];

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

  /**
   * Constructs a VideoPlayerFormatter 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\ffmpeg_media_player\Service\PlayerConfigResolverInterface $playerConfigResolver
   *   Player config resolver service.
   * @param \Drupal\ffmpeg_media\Service\ProfileManagerInterface $profileManager
   *   Profile manager service.
   * @param \Drupal\ffmpeg_media\Resolver\VideoSourceResolverInterface $videoSourceResolver
   *   Video source resolver service.
   */
  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 = NULL,
    #[Autowire(service: ProgressTrackerInterface::class)]
    protected readonly ?ProgressTrackerInterface $progressTracker = NULL,
    #[Autowire(service: 'config.factory')]
    protected readonly ?ConfigFactoryInterface $configFactory = NULL,
    #[Autowire(service: PlayerConfigResolverInterface::class)]
    protected readonly ?PlayerConfigResolverInterface $playerConfigResolver = NULL,
    #[Autowire(service: ProfileManagerInterface::class)]
    protected readonly ?ProfileManagerInterface $profileManager = NULL,
    #[Autowire(service: VideoSourceResolverInterface::class)]
    protected readonly ?VideoSourceResolverInterface $videoSourceResolver = 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 {
    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'),
      $container->get(PlayerConfigResolverInterface::class),
      $container->get(ProfileManagerInterface::class),
      $container->get(VideoSourceResolverInterface::class),
    );
  }

  /**
   * {@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),
    );
  }

  /**
   * {@inheritdoc}
   *
   * @return array<string, mixed>
   *   Default formatter settings.
   */
  #[\Override]
  public static function defaultSettings(): array {
    return [
      'auto_convert' => TRUE,
      'conversion_profile' => '',
      'autoplay' => FALSE,
      'muted' => FALSE,
      'loop' => FALSE,
      'controls' => TRUE,
      'preload' => 'metadata',
      'format_priority' => ['hls', 'dash', 'mp4', 'webm'],
      'player_theme' => 'default',
      'aspect_ratio' => '16:9',
      'lazy_load' => TRUE,
      'show_progress' => TRUE,
      'stream_type' => 'on-demand',
    ] + 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']],
      'message' => [
        '#markup' => $this->t('Full-featured player supporting all common video formats and adaptive streaming (HLS, DASH) with quality switching, subtitles, and keyboard navigation.'),
      ],
      '#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,
    ];

    $profileOptions = ['' => $this->t('- Default -')];
    if ($this->profileManager !== NULL) {
      $profiles = $this->profileManager->getAllProfiles();
      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['player_theme'] = [
      '#type' => 'select',
      '#title' => $this->t('Player theme'),
      '#options' => [
        'default' => $this->t('Default'),
        'minimal' => $this->t('Minimal'),
      ],
      '#default_value' => $this->getSetting('player_theme'),
    ];

    $elements['aspect_ratio'] = [
      '#type' => 'select',
      '#title' => $this->t('Aspect ratio'),
      '#options' => [
        'auto' => $this->t('Auto (intrinsic video dimensions)'),
        '16:9' => $this->t('16:9 (Widescreen)'),
        '4:3' => $this->t('4:3 (Standard)'),
        '21:9' => $this->t('21:9 (Ultrawide)'),
        '1:1' => $this->t('1:1 (Square)'),
        '9:16' => $this->t('9:16 (Portrait)'),
      ],
      '#default_value' => $this->getSetting('aspect_ratio'),
    ];

    $profileId = $this->getSetting('conversion_profile')
      ?: ($this->profileManager?->getDefaultProfileId()
        ?? ProfileManagerInterface::DEFAULT_PROFILE);
    $defaultProfileId = $this->profileManager?->getDefaultProfileId()
      ?? ProfileManagerInterface::DEFAULT_PROFILE;

    // Build format options for all profiles so JS can swap them.
    $allProfileFormats = [];
    if ($this->profileManager !== NULL) {
      foreach ($profiles as $profile) {
        $allProfileFormats[$profile->id] = $this->buildFormatOptions($profile->id);
      }
    }
    $allProfileFormats[''] = $allProfileFormats[$defaultProfileId] ?? [];

    // Current profile's formats for initial rendering.
    $currentProfileFormats = $this->buildFormatOptions($profileId);

    // Union of all formats so all checkboxes are pre-rendered.
    $allFormats = $currentProfileFormats;
    foreach ($allProfileFormats as $pFormats) {
      foreach ($pFormats as $key => $label) {
        if (!isset($allFormats[$key])) {
          $allFormats[$key] = $label;
        }
      }
    }

    $savedPriority = array_filter(
      $this->getSetting('format_priority')
      ?? ['hls', 'dash', 'mp4', 'webm'],
    );
    $defaultValue = $this->normalizeFormatPriority(
      $profileId,
      $savedPriority,
    );
    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,
      ],
    ];

    // Attach JS for dynamic format updates.
    $elements['#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',
          ],
        ];
      }
    }

    $elements['lazy_load'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Lazy load'),
      '#description' => $this->t('Initialize player only when visible in viewport (better Core Web Vitals).'),
      '#default_value' => $this->getSetting('lazy_load'),
    ];

    $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'),
    ];

    $elements['stream_type'] = [
      '#type' => 'select',
      '#title' => $this->t('Stream type'),
      '#options' => [
        'on-demand' => $this->t('On-demand'),
        'live' => $this->t('Live'),
        'live:dvr' => $this->t('Live DVR'),
      ],
      '#default_value' => $this->getSetting('stream_type'),
    ];

    return $elements;
  }

  /**
   * Builds format options from a profile's enabled outputs.
   *
   * @param string $profileId
   *   The profile ID.
   *
   * @return array<string, string>
   *   Format options keyed by output key.
   */
  #[\NoDiscard]
  protected function buildFormatOptions(string $profileId): array {
    if ($this->profileManager === NULL) {
      return [];
    }

    $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);

      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;
  }

  /**
   * 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 {
    if ($this->profileManager === NULL) {
      return;
    }

    $selected = array_filter(
      $form_state->getValue($element['#parents']) ?? [],
    );

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

    $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)],
      ));
    }
  }

  /**
   * {@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()
        ?? ProfileManagerInterface::DEFAULT_PROFILE);
    $profileData = $this->profileManager?->getProfile($profileId);
    $profileLabel = $profileData?->label ?? $profileId;
    $summary[] = $this->t('Profile: @profile', [
      '@profile' => $profileLabel,
    ]);

    $summary[] = $this->t('Theme: @theme', [
      '@theme' => ucfirst($this->getSetting('player_theme')),
    ]);

    $summary[] = $this->t('Aspect ratio: @ratio', [
      '@ratio' => $this->getSetting('aspect_ratio'),
    ]);

    $features = [];
    if ($this->getSetting('controls')) {
      $features[] = $this->t('controls');
    }
    if ($this->getSetting('lazy_load')) {
      $features[] = $this->t('lazy load');
    }
    if (!empty($features)) {
      $summary[] = $this->t('Features: @features', [
        '@features' => implode(', ', $features),
      ]);
    }

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

      if ($this->profileManager !== NULL) {
        $supportedSelections = $this->getSupportedFormatSelections($profileId);
        $unavailable = array_diff($formats, $supportedSelections);
        if ($unavailable !== []) {
          $summary[] = $this->t('Warning: Formats @formats not available in profile', [
            '@formats' => implode(', ', array_map(static fn(string $format): string => ConversionProfile::labelFromKey($format), $unavailable)),
          ]);
        }
      }
    }

    return $summary;
  }

  /**
   * {@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;
    }

    if ($this->progressTracker === NULL || $this->playerConfigResolver === NULL || $this->videoSourceResolver === NULL) {
      return $elements;
    }

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

    // Resolve video sources, poster, and subtitles via VideoSourceResolver.
    $profileId = $this->getSetting('conversion_profile') ?: NULL;
    $formatPriority = $this->normalizeFormatPriority(
      $profileId,
      array_values(array_filter(
        $this->getSetting('format_priority') ?? [],
      )),
    );

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

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

    // 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();

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

    $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,
            ],
          ],
        ],
      ],
      '#cache' => [
        'tags' => [...$resolved->cacheTags, 'config:ffmpeg_media.settings', 'config:ffmpeg_media_player.settings'],
        'contexts' => ['user.permissions', 'url.site'],
        'max-age' => ($progress !== NULL && $progress->isInProgress) ? 0 : -1,
      ],
    ];

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

    return $elements;
  }

  /**
   * Normalizes saved format selections to the profile's output-key model.
   *
   * Expands legacy container selections such as `mp4` to all matching
   * codec-qualified keys like `mp4:h264` and `mp4:av1` when a profile has
   * duplicate containers.
   *
   * @param string|null $profileId
   *   The configured profile ID, or NULL to use the default profile.
   * @param array<int, string> $selectedFormats
   *   Saved format selections.
   *
   * @return array<int, string>
   *   Normalized output keys in profile order.
   */
  #[\NoDiscard]
  private function normalizeFormatPriority(?string $profileId, array $selectedFormats): array {
    if ($selectedFormats === [] || $this->profileManager === NULL) {
      return array_values($selectedFormats);
    }

    $resolvedProfileId = $profileId ?: $this->profileManager->getDefaultProfileId();
    $profileKeys = $this->profileManager->getEnabledOutputKeys($resolvedProfileId);
    $normalized = [];

    foreach ($selectedFormats as $selectedFormat) {
      if (in_array($selectedFormat, $profileKeys, TRUE)) {
        if (!in_array($selectedFormat, $normalized, TRUE)) {
          $normalized[] = $selectedFormat;
        }
        continue;
      }

      if (ConversionProfile::isQualifiedKey($selectedFormat)) {
        continue;
      }

      foreach ($profileKeys as $profileKey) {
        if (ConversionProfile::containerFromKey($profileKey) !== $selectedFormat) {
          continue;
        }
        if (!in_array($profileKey, $normalized, TRUE)) {
          $normalized[] = $profileKey;
        }
      }
    }

    return $normalized;
  }

  /**
   * Returns all supported saved-selection tokens for a profile.
   *
   * Includes canonical output keys and legacy plain container names so older
   * saved settings do not produce false warnings after key normalization.
   *
   * @param string $profileId
   *   The profile ID.
   *
   * @return array<int, string>
   *   Supported format selection tokens.
   */
  #[\NoDiscard]
  private function getSupportedFormatSelections(string $profileId): array {
    if ($this->profileManager === NULL) {
      return [];
    }

    $profileKeys = $this->profileManager->getEnabledOutputKeys($profileId);
    $supported = $profileKeys;

    foreach ($profileKeys as $profileKey) {
      $container = ConversionProfile::containerFromKey($profileKey);
      if (!in_array($container, $supported, TRUE)) {
        $supported[] = $container;
      }
    }

    return $supported;
  }

}
