<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media_ui\Form;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\Core\Url;
use Drupal\ffmpeg_media\Enum\OutputType;
use Drupal\ffmpeg_media\Service\ConfigValueProviderInterface;
use Drupal\ffmpeg_media\Service\OutputStorageServiceInterface;
use Drupal\ffmpeg_media_image\Service\TemporaryFileAccessManagerInterface;
use Drupal\ffmpeg_media_image\Service\ThumbnailGeneratorServiceInterface;
use Drupal\ffmpeg_media_ui\Service\MediaOperationsService;
use Drupal\ffmpeg_media_ui\Service\MediaOperationsServiceInterface;
use Drupal\file\FileInterface;
use Drupal\media\MediaInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

/**
 * Form for selecting a poster frame from video thumbnails.
 */
final class PosterSelectorForm extends FormBase {

  /**
   * The media entity.
   */
  protected ?MediaInterface $media = NULL;

  /**
   * Canonical source field name used for this form flow.
   */
  protected string $fieldName = '';

  /**
   * Cached preview base directory URI.
   */
  private ?string $previewBaseDir = NULL;

  /**
   * Constructs a PosterSelectorForm object.
   *
   * @param \Drupal\ffmpeg_media_ui\Service\MediaOperationsServiceInterface $mediaOperations
   *   Media operations service.
   * @param \Drupal\ffmpeg_media_image\Service\ThumbnailGeneratorServiceInterface $thumbnailGenerator
   *   Thumbnail generator service.
   * @param \Drupal\ffmpeg_media_image\Service\TemporaryFileAccessManagerInterface $temporaryFileAccessManager
   *   Temporary file access manager.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
   *   File system service.
   * @param \Drupal\Core\File\FileUrlGeneratorInterface $fileUrlGenerator
   *   File URL generator service.
   * @param \Drupal\ffmpeg_media\Service\OutputStorageServiceInterface $outputStorage
   *   Output storage service.
   * @param \Drupal\ffmpeg_media\Service\ConfigValueProviderInterface $configValueProvider
   *   Config value provider for output directory.
   * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
   *   Stream wrapper manager.
   */
  public function __construct(
    protected readonly MediaOperationsServiceInterface $mediaOperations,
    protected readonly ThumbnailGeneratorServiceInterface $thumbnailGenerator,
    protected readonly TemporaryFileAccessManagerInterface $temporaryFileAccessManager,
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly FileSystemInterface $fileSystem,
    protected readonly FileUrlGeneratorInterface $fileUrlGenerator,
    protected readonly OutputStorageServiceInterface $outputStorage,
    protected readonly ConfigValueProviderInterface $configValueProvider,
    protected readonly StreamWrapperManagerInterface $streamWrapperManager,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get(MediaOperationsService::class),
      $container->get(ThumbnailGeneratorServiceInterface::class),
      $container->get(TemporaryFileAccessManagerInterface::class),
      $container->get('entity_type.manager'),
      $container->get('file_system'),
      $container->get('file_url_generator'),
      $container->get(OutputStorageServiceInterface::class),
      $container->get(ConfigValueProviderInterface::class),
      $container->get('stream_wrapper_manager'),
    );
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function getFormId(): string {
    return 'ffmpeg_media_poster_selector_form';
  }

  /**
   * {@inheritdoc}
   *
   * @return array<string, mixed>
   *   The form render array.
   */
  #[\Override]
  public function buildForm(array $form, FormStateInterface $form_state, ?MediaInterface $media = NULL): array {
    $this->media = $media;

    if ($media === NULL) {
      return [
        '#markup' => $this->t('Media not found.'),
      ];
    }
    if (!$media->access('update')) {
      throw new AccessDeniedHttpException('Access denied for this media item.');
    }

    $this->fieldName = $this->mediaOperations->resolveFieldName(
      $media,
      (string) $this->getRequest()->query->get('field_name', ''),
    );

    $videoPath = $this->mediaOperations->getSourceFile($media, $this->fieldName);
    $videoInfo = $this->mediaOperations->getVideoInfo($media, $this->fieldName);

    if ($videoPath === NULL || $videoInfo === NULL) {
      return [
        '#markup' => $this->t('Could not read video file.'),
      ];
    }

    $form['#attached']['library'][] = 'ffmpeg_media_ui/poster-selector';
    $form['#attached']['drupalSettings']['ffmpegPosterSelector'] = [
      'mediaId' => $media->id(),
      'duration' => $videoInfo['duration'],
      'ajaxUrl' => Url::fromRoute('ffmpeg_media_ui.ajax.thumbnails', ['media' => $media->id()], $this->fieldName !== '' ? ['query' => ['field_name' => $this->fieldName]] : [])->toString(),
      'ajaxMethod' => 'POST',
      'csrfTokenUrl' => Url::fromRoute('system.csrftoken')->toString(),
    ];

    $form['#attributes']['class'][] = 'ffmpeg-poster-selector-form';

    // Header.
    $form['header'] = [
      '#type' => 'container',
      '#attributes' => ['class' => ['ffmpeg-poster-header']],
      'title' => [
        '#type' => 'html_tag',
        '#tag' => 'h3',
        '#value' => $this->t('Select Poster Frame for "@title"', ['@title' => $media->label()]),
      ],
      'duration' => [
        '#type' => 'html_tag',
        '#tag' => 'p',
        '#value' => $this->t('Video duration: @duration', [
          '@duration' => gmdate('H:i:s', (int) $videoInfo['duration']),
        ]),
        '#attributes' => ['class' => ['ffmpeg-video-duration']],
      ],
    ];

    // Thumbnail grid - will be populated via AJAX or pre-generated.
    $thumbnails = $this->thumbnailGenerator->generateThumbnails(
      $videoPath,
      $videoInfo['duration'],
    );
    $this->registerTemporaryImageUris($thumbnails, (int) $media->id());

    $form['thumbnail_grid'] = [
      '#type' => 'container',
      '#attributes' => [
        'class' => ['ffmpeg-thumbnail-grid'],
        'id' => 'ffmpeg-thumbnail-grid',
        'role' => 'listbox',
        'aria-label' => $this->t('Poster frame selection'),
      ],
    ];

    foreach ($thumbnails as $index => $thumbnail) {
      $form['thumbnail_grid']['thumb_' . $index] = [
        '#type' => 'container',
        '#attributes' => [
          'class' => ['ffmpeg-thumbnail-item'],
          'data-timestamp' => $thumbnail['timestamp'],
          'data-index' => $index,
          'role' => 'option',
          'aria-selected' => 'false',
          'tabindex' => $index === 0 ? '0' : '-1',
        ],
        'image' => [
          '#type' => 'html_tag',
          '#tag' => 'img',
          '#attributes' => [
            'src' => $thumbnail['url'],
            'alt' => $this->t('Frame at @time', ['@time' => $thumbnail['time_formatted']]),
            'class' => ['ffmpeg-thumbnail-image'],
            'loading' => 'lazy',
          ],
        ],
        'time' => [
          '#type' => 'html_tag',
          '#tag' => 'span',
          '#value' => $thumbnail['time_formatted'],
          '#attributes' => ['class' => ['ffmpeg-thumbnail-time']],
        ],
      ];
    }

    // Selected timestamp (hidden field).
    $form['selected_timestamp'] = [
      '#type' => 'hidden',
      '#default_value' => '',
      '#attributes' => ['id' => 'ffmpeg-selected-timestamp'],
    ];

    // Custom timestamp input.
    $form['custom_time'] = [
      '#type' => 'details',
      '#title' => $this->t('Custom Time'),
      '#open' => FALSE,
      '#attributes' => ['class' => ['ffmpeg-custom-time']],
    ];

    $form['custom_time']['custom_timestamp'] = [
      '#type' => 'textfield',
      '#title' => $this->t('Enter time (MM:SS or HH:MM:SS)'),
      '#size' => 10,
      '#placeholder' => '0:30',
      '#description' => $this->t('Enter a specific time to use as the poster frame.'),
    ];

    $form['custom_time']['preview_custom'] = [
      '#type' => 'button',
      '#value' => $this->t('Preview'),
      '#ajax' => [
        'callback' => '::previewCustomTimestamp',
        'wrapper' => 'custom-preview-wrapper',
      ],
    ];

    $form['custom_time']['preview'] = [
      '#type' => 'container',
      '#attributes' => ['id' => 'custom-preview-wrapper'],
    ];

    // Actions.
    $form['actions'] = [
      '#type' => 'actions',
    ];

    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Set as Poster'),
      '#button_type' => 'primary',
      '#attributes' => ['class' => ['ffmpeg-set-poster-btn']],
    ];

    $cancelRouteOptions = $this->fieldName !== '' ? ['query' => ['field_name' => $this->fieldName]] : [];
    $form['actions']['cancel'] = [
      '#type' => 'link',
      '#title' => $this->t('Cancel'),
      '#url' => Url::fromRoute('ffmpeg_media_ui.transcode', ['media' => $media->id()], $cancelRouteOptions),
      '#attributes' => ['class' => ['button']],
    ];

    // Store thumbnails in form state for cleanup.
    $form_state->set('thumbnails', $thumbnails);

    return $form;
  }

  /**
   * AJAX callback to preview a custom timestamp.
   *
   * @return array<string, mixed>
   *   Render array for the preview.
   */
  public function previewCustomTimestamp(array &$form, FormStateInterface $form_state): array {
    $customTime = $form_state->getValue('custom_timestamp');
    $timestamp = $this->parseTimeString($customTime);

    if ($timestamp === NULL || $this->media === NULL) {
      return [
        '#markup' => $this->t('Invalid time format.'),
      ];
    }
    if (!$this->media->access('update')) {
      throw new AccessDeniedHttpException('Access denied for this media item.');
    }

    $this->fieldName = $this->mediaOperations->resolveFieldName($this->media, $this->fieldName);

    $videoPath = $this->mediaOperations->getSourceFile($this->media, $this->fieldName);
    if ($videoPath === NULL) {
      return [
        '#markup' => $this->t('Video file not found.'),
      ];
    }

    // Generate a preview thumbnail, preferring private:// when available.
    $previewDir = $this->getPreviewBaseDir();
    $previewUri = $previewDir . '/custom_preview_' . bin2hex(random_bytes(16)) . '.jpg';
    $this->fileSystem->prepareDirectory($previewDir, FileSystemInterface::CREATE_DIRECTORY);
    $previewDirRealPath = $this->fileSystem->realpath($previewDir);
    if ($previewDirRealPath === FALSE) {
      return [
        '#markup' => $this->t('Failed to prepare preview directory.'),
      ];
    }
    $previewRealPath = $previewUri
      |> basename(...)
      |> (fn(string $filename): string => $previewDirRealPath . '/' . $filename);

    $success = $this->thumbnailGenerator->generateSingleThumbnail(
      $videoPath,
      $previewRealPath,
      $timestamp,
      320,
    );

    if (!$success) {
      return [
        '#markup' => $this->t('Failed to generate preview.'),
      ];
    }

    $this->temporaryFileAccessManager->registerUri($previewUri, (int) $this->media->id());

    // Track custom preview paths for cleanup on form abandonment.
    $customPreviews = $form_state->get('custom_previews') ?? [];
    $customPreviews[] = $previewUri;
    $form_state->set('custom_previews', $customPreviews);

    return [
      '#type' => 'container',
      '#attributes' => ['class' => ['ffmpeg-custom-preview']],
      'image' => [
        '#type' => 'html_tag',
        '#tag' => 'img',
        '#attributes' => [
          'src' => $this->fileUrlGenerator->generateAbsoluteString($previewUri),
          'alt' => $this->t('Preview at @time', ['@time' => $customTime]),
        ],
      ],
      'use_button' => [
        '#type' => 'html_tag',
        '#tag' => 'button',
        '#value' => $this->t('Use This Frame'),
        '#attributes' => [
          'type' => 'button',
          'class' => ['button', 'ffmpeg-use-custom-btn'],
          'data-timestamp' => $timestamp,
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function validateForm(array &$form, FormStateInterface $form_state): void {
    parent::validateForm($form, $form_state);

    $customTime = trim((string) ($form_state->getValue('custom_timestamp') ?? ''));
    $selectedTimestamp = $form_state->getValue('selected_timestamp');

    if ($customTime !== '' && ($selectedTimestamp === '' || $selectedTimestamp === NULL)) {
      $parsed = $this->parseTimeString($customTime);
      if ($parsed === NULL) {
        $form_state->setErrorByName('custom_timestamp', $this->t('Invalid time format. Use MM:SS, HH:MM:SS, or seconds.'));
      }
      elseif ($parsed < 0) {
        $form_state->setErrorByName('custom_timestamp', $this->t('Time must not be negative.'));
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function submitForm(array &$form, FormStateInterface $form_state): void {
    if ($this->media === NULL) {
      $this->messenger()->addError($this->t('Media not found.'));
      return;
    }
    if (!$this->media->access('update')) {
      throw new AccessDeniedHttpException('Access denied for this media item.');
    }
    $media = $this->media;
    $this->fieldName = $this->mediaOperations->resolveFieldName($media, $this->fieldName);

    // Get selected timestamp.
    $timestamp = $form_state->getValue('selected_timestamp');
    $customTime = $form_state->getValue('custom_timestamp');

    if (($timestamp === '' || $timestamp === NULL) && !empty($customTime)) {
      $timestamp = $this->parseTimeString($customTime);
    }

    if ($timestamp === '' || $timestamp === NULL || !is_numeric((string) $timestamp)) {
      $this->messenger()->addError($this->t('Please select a frame.'));
      return;
    }

    // Generate high-quality poster from the scoped field's source.
    $videoPath = $this->mediaOperations->getSourceFile($media, $this->fieldName);
    if ($videoPath === NULL) {
      $this->messenger()->addError($this->t('Video file not found.'));
      return;
    }

    $posterDir = $this->configValueProvider->getOutputDirectory() . '/posters';
    $this->fileSystem->prepareDirectory($posterDir, FileSystemInterface::CREATE_DIRECTORY);

    // Use a unique filename per selection to prevent stale browser/cache hits
    // and avoid deleting a new poster when cleaning up old entities.
    $timestampToken = number_format((float) $timestamp, 3, '.', '');
    $timestampToken = rtrim(rtrim($timestampToken, '0'), '.');
    $timestampToken = str_replace('.', '_', $timestampToken);
    $posterFilename = $videoPath
      |> (fn(string $path): string => pathinfo($path, PATHINFO_FILENAME))
      |> (fn(string $baseName): string => sprintf(
        '%s_poster_t%s_%s.jpg',
        $baseName,
        $timestampToken !== '' ? $timestampToken : '0',
        bin2hex(random_bytes(16)),
      ));
    $posterPath = $posterDir . '/' . $posterFilename;
    $posterDirRealPath = $this->fileSystem->realpath($posterDir);
    if ($posterDirRealPath === FALSE) {
      $this->messenger()->addError($this->t('Failed to prepare poster directory.'));
      return;
    }
    $realPath = $posterDirRealPath . '/' . $posterFilename;

    $success = $this->thumbnailGenerator->generatePoster(
      $videoPath,
      $realPath,
      (float) $timestamp,
    );

    if (!$success) {
      $this->messenger()->addError($this->t('Failed to generate poster image.'));
      return;
    }

    // Save as file entity and store in output table.
    if (!$this->storePosterOutput($posterPath, $media, $this->fieldName)) {
      $this->messenger()->addError($this->t('Failed to store poster image.'));
      return;
    }

    // Cleanup temp thumbnails and custom preview files.
    $thumbnails = $form_state->get('thumbnails') ?? [];
    $this->thumbnailGenerator->cleanup($thumbnails);

    $customPreviews = $form_state->get('custom_previews') ?? [];
    foreach ($customPreviews as $previewPath) {
      if (is_string($previewPath)) {
        try {
          $this->fileSystem->delete($previewPath);
        }
        catch (\Exception) {
          // Best-effort cleanup; orphans are caught by cron temp cleanup.
        }
      }
    }

    $this->messenger()->addStatus($this->t('Poster image has been set.'));
    $this->setPostSubmitRedirect($form_state, $media);
  }

  /**
   * Returns the base directory URI for preview images.
   *
   * Prefers private:// when available for access-controlled serving.
   * Falls back to public://.
   *
   * @return string
   *   The stream URI for the preview base directory.
   */
  protected function getPreviewBaseDir(): string {
    if ($this->previewBaseDir !== NULL) {
      return $this->previewBaseDir;
    }

    if ($this->streamWrapperManager->getViaScheme('private') !== FALSE) {
      $this->previewBaseDir = 'private://ffmpeg_media/previews';
    }
    else {
      $this->previewBaseDir = 'public://ffmpeg_media/previews';
    }

    return $this->previewBaseDir;
  }

  /**
   * Registers temporary private image URIs for media-based download access.
   *
   * @param array<int, array{uri?: string}> $items
   *   Temporary image metadata.
   * @param int $mediaId
   *   The related media ID.
   */
  protected function registerTemporaryImageUris(array $items, int $mediaId): void {
    foreach ($items as $item) {
      $uri = $item['uri'] ?? NULL;
      if (is_string($uri) && str_starts_with($uri, 'private://')) {
        $this->temporaryFileAccessManager->registerUri($uri, $mediaId);
      }
    }
  }

  /**
   * Stores a poster image via the output storage service.
   *
   * @param string $posterUri
   *   Poster file URI.
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param string $fieldName
   *   The source field name for output storage.
   *
   * @return bool
   *   TRUE if stored successfully.
   */
  protected function storePosterOutput(string $posterUri, MediaInterface $media, string $fieldName): bool {
    $mediaId = (int) $media->id();

    // Keep reference to previous poster for cleanup (check both field-scoped
    // and legacy empty-field-name entries).
    $oldFile = $this->outputStorage->getOutput($mediaId, $fieldName, OutputType::Thumbnail->value);
    $legacyFile = $fieldName !== '' ? $this->outputStorage->getOutput($mediaId, '', OutputType::Thumbnail->value) : NULL;

    // Create new file entity.
    /** @var \Drupal\file\FileInterface $file */
    $file = $this->entityTypeManager->getStorage('file')->create([
      'uri' => $posterUri,
      'filename' => basename($posterUri),
      'status' => 1,
    ]);
    $file->save();

    // Store in output table with the actual field name.
    $this->outputStorage->setOutput($mediaId, $fieldName, OutputType::Thumbnail->value, $file);

    // Remove legacy empty-field-name entry if it exists.
    if ($legacyFile instanceof FileInterface) {
      $this->outputStorage->deleteOutput($mediaId, '', OutputType::Thumbnail->value);
      if ($legacyFile->id() !== $file->id() && ($oldFile === NULL || $legacyFile->id() !== $oldFile->id())) {
        $legacyFile->delete();
      }
    }

    // Cleanup old poster file after successful storage.
    if ($oldFile instanceof FileInterface && $oldFile->id() !== $file->id()) {
      $oldFile->delete();
    }

    return TRUE;
  }

  /**
   * Sets a post-submit redirect without forcing users into 403 pages.
   */
  protected function setPostSubmitRedirect(FormStateInterface $form_state, MediaInterface $media): void {
    $routeOptions = $this->fieldName !== '' ? ['query' => ['field_name' => $this->fieldName]] : [];
    if ($this->currentUser()->hasPermission('access ffmpeg media ui') && $media->access('view')) {
      $form_state->setRedirect('ffmpeg_media_ui.status', ['media' => $media->id()], $routeOptions);
      return;
    }

    $form_state->setRedirect('entity.media.edit_form', ['media' => $media->id()]);
  }

  /**
   * Parses a time string to seconds.
   *
   * @param string $timeString
   *   Time in MM:SS or HH:MM:SS format.
   *
   * @return float|null
   *   Seconds or NULL if invalid.
   */
  protected function parseTimeString(string $timeString): ?float {
    $timeString = trim($timeString);

    // Try HH:MM:SS.
    if (preg_match('/^(\d+):(\d{2}):(\d{2})$/', $timeString, $matches)) {
      return (int) $matches[1] * 3600 + (int) $matches[2] * 60 + (int) $matches[3];
    }

    // Try MM:SS.
    if (preg_match('/^(\d+):(\d{2})$/', $timeString, $matches)) {
      return (int) $matches[1] * 60 + (int) $matches[2];
    }

    // Try seconds only.
    if (is_numeric($timeString)) {
      return (float) $timeString;
    }

    return NULL;
  }

}
