<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media_ui\Form;

use Drupal\Component\Utility\Html;
use Drupal\Core\Ajax\AjaxResponse;
use Drupal\Core\Ajax\InvokeCommand;
use Drupal\Core\Ajax\ReplaceCommand;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Url;
use Drupal\ffmpeg_media\Service\FFmpegExecutorInterface;
use Drupal\ffmpeg_media\Service\ProfileManagerInterface;
use Drupal\ffmpeg_media_ui\Service\MediaOperationsService;
use Drupal\media\MediaInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;

/**
 * Form for transcoding a media item with options.
 */
final class TranscodeForm extends FormBase {

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

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

  /**
   * Constructs a TranscodeForm object.
   *
   * @param \Drupal\ffmpeg_media_ui\Service\MediaOperationsService $mediaOperations
   *   Media operations service.
   * @param \Drupal\ffmpeg_media\Service\ProfileManagerInterface $profileManager
   *   Profile manager service.
   * @param \Drupal\ffmpeg_media\Service\FFmpegExecutorInterface $ffmpegExecutor
   *   FFmpeg executor service.
   */
  public function __construct(
    protected readonly MediaOperationsService $mediaOperations,
    protected readonly ProfileManagerInterface $profileManager,
    protected readonly FFmpegExecutorInterface $ffmpegExecutor,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public static function create(ContainerInterface $container): static {
    return new static(
      $container->get(MediaOperationsService::class),
      $container->get(ProfileManagerInterface::class),
      $container->get(FFmpegExecutorInterface::class),
    );
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function getFormId(): string {
    return 'ffmpeg_media_transcode_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', ''),
    );

    if (!$this->ffmpegExecutor->isAvailable()) {
      $this->messenger()->addError($this->t('FFmpeg is not available. Please check your <a href=":url">module settings</a> and ensure FFmpeg is installed and the path is correctly configured.', [
        ':url' => Url::fromRoute('ffmpeg_media.settings')->toString(),
      ]));
    }

    $isQueued = $this->mediaOperations->isQueued($media, $this->fieldName);
    $status = $this->mediaOperations->getStatus($media, $this->fieldName);
    $videoInfo = $this->mediaOperations->getVideoInfo($media, $this->fieldName);
    $outputs = $this->mediaOperations->getOutputs($media, $this->fieldName);

    $form['#attached']['library'][] = 'ffmpeg_media_ui/transcode-form';

    // Media info section.
    $form['media_info'] = [
      '#type' => 'container',
      '#attributes' => ['class' => ['ffmpeg-media-info']],
    ];

    $form['media_info']['title'] = [
      '#type' => 'html_tag',
      '#tag' => 'h3',
      '#value' => Html::escape((string) $media->label()),
    ];

    // Video details.
    if ($videoInfo !== NULL) {
      $form['media_info']['video_details'] = [
        '#type' => 'container',
        '#attributes' => ['class' => ['ffmpeg-video-details']],
        'items' => [
          '#theme' => 'item_list',
          '#items' => $this->formatVideoInfo($videoInfo),
          '#attributes' => ['class' => ['ffmpeg-details-list']],
        ],
      ];
    }

    // Queued state: show cancel button.
    if ($isQueued) {
      $form['queued_status'] = [
        '#type' => 'container',
        '#attributes' => ['class' => ['messages', 'messages--warning']],
        '#markup' => $this->t('This media is currently queued for transcoding. You can cancel and re-queue with different settings.'),
      ];

      $form['cancel_queued'] = [
        '#type' => 'submit',
        '#value' => $this->t('Cancel Queued Transcoding'),
        '#submit' => ['::cancelQueuedTranscoding'],
        '#attributes' => ['class' => ['button', 'button--danger']],
        '#limit_validation_errors' => [],
      ];
    }

    // Current status.
    if ($status['status'] !== 'none') {
      $form['current_status'] = [
        '#type' => 'container',
        '#attributes' => ['class' => ['ffmpeg-current-status']],
        'progress' => [
          '#theme' => 'ffmpeg_media_progress_bar',
          '#progress' => $status['progress'],
          '#status' => $status['status'],
          '#message' => $status['message'],
          '#attached' => ['library' => ['ffmpeg_media_ui/progress-bar']],
        ],
      ];

      if ($status['status'] === 'processing') {
        $form['current_status']['warning'] = [
          '#type' => 'container',
          '#attributes' => ['class' => ['messages', 'messages--warning']],
          '#markup' => $this->t('This media is currently being processed. Starting a new transcode will add it to the queue.'),
        ];
      }
    }

    // Existing outputs.
    if (!empty($outputs)) {
      $form['existing_outputs'] = [
        '#type' => 'details',
        '#title' => $this->t('Existing Outputs (@count)', ['@count' => count($outputs)]),
        '#open' => FALSE,
        'list' => [
          '#theme' => 'ffmpeg_media_output_list',
          '#outputs' => $outputs,
          '#attached' => ['library' => ['ffmpeg_media_ui/output-list']],
        ],
      ];
    }

    // Transcode options.
    $form['options'] = [
      '#type' => 'details',
      '#title' => $this->t('Transcode Options'),
      '#open' => TRUE,
      '#tree' => TRUE,
      '#description' => $this->t('Videos are automatically converted when uploaded, using the profile configured in <em>Manage Display</em>. Use this form to re-convert this video with different settings or to trigger a new conversion manually.'),
    ];

    // Profile selection.
    $profiles = $this->profileManager->getAllProfiles();
    $profileOptions = [];
    foreach ($profiles as $id => $profile) {
      $profileOptions[$id] = $profile->label;
    }

    $defaultProfile = $this->profileManager->getDefaultProfileId();
    $form['options']['profile'] = [
      '#type' => 'select',
      '#title' => $this->t('Conversion Profile'),
      '#options' => $profileOptions,
      '#default_value' => $defaultProfile,
      '#description' => $this->t('Choose a profile for this conversion. This overrides the profile from <em>Manage Display</em> for this single operation only.'),
      '#ajax' => [
        'callback' => '::updateOutputOptions',
        'wrapper' => 'output-formats-wrapper',
      ],
    ];

    // Output format selection.
    $form['options']['outputs'] = [
      '#type' => 'fieldset',
      '#title' => $this->t('Output Formats'),
      '#description' => $this->t('Select which formats to generate.'),
      '#prefix' => '<div id="output-formats-wrapper" tabindex="-1">',
      '#suffix' => '</div>',
    ];

    $selectedProfile = $form_state->getValue(['options', 'profile']) ?? $defaultProfile;
    $profile = $profiles[$selectedProfile] ?? NULL;
    $profileOutputs = $profile?->outputs ?? [];

    foreach ($profileOutputs as $output) {
      $form['options']['outputs'][$output->container] = [
        '#type' => 'checkbox',
        '#title' => strtoupper($output->container),
        '#default_value' => $output->enabled,
      ];
    }

    // Clear existing outputs.
    $form['options']['clear_existing'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Clear existing outputs before transcoding'),
      '#default_value' => FALSE,
      '#description' => $this->t('Remove all existing transcoded files before starting.'),
    ];

    // Force re-transcode.
    $form['options']['force'] = [
      '#type' => 'checkbox',
      '#title' => $this->t('Force re-transcode'),
      '#default_value' => FALSE,
      '#description' => $this->t('Transcode even if outputs already exist.'),
    ];

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

    $form['actions']['submit'] = [
      '#type' => 'submit',
      '#value' => $this->t('Start Transcode'),
      '#button_type' => 'primary',
      '#access' => !$isQueued,
    ];

    $posterRouteOptions = $this->fieldName !== '' ? ['query' => ['field_name' => $this->fieldName]] : [];
    $form['actions']['poster'] = [
      '#type' => 'link',
      '#title' => $this->t('Select Poster Frame'),
      '#url' => Url::fromRoute('ffmpeg_media_ui.poster_select', ['media' => $media->id()], $posterRouteOptions),
      '#attributes' => ['class' => ['button']],
    ];

    $form['actions']['cancel'] = [
      '#type' => 'link',
      '#title' => $this->t('Cancel'),
      '#url' => Url::fromRoute('ffmpeg_media_ui.dashboard'),
      '#attributes' => ['class' => ['button']],
    ];

    return $form;
  }

  /**
   * AJAX callback to update output options when profile changes.
   *
   * @param array $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   *
   * @return \Drupal\Core\Ajax\AjaxResponse
   *   AJAX response with replace and focus commands.
   */
  public function updateOutputOptions(array &$form, FormStateInterface $form_state): AjaxResponse {
    $response = new AjaxResponse();
    $response->addCommand(new ReplaceCommand('#output-formats-wrapper', $form['options']['outputs']));
    // Restore focus for keyboard/screen reader users after AJAX update.
    $response->addCommand(new InvokeCommand('#output-formats-wrapper', 'focus'));
    return $response;
  }

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

    if ($this->media === NULL) {
      $form_state->setErrorByName('', $this->t('Media entity not found.'));
      return;
    }

    $optionsValues = $form_state->getValue('options') ?? [];
    $profileId = $optionsValues['profile'] ?? '';
    if ($profileId !== '' && $this->profileManager->getProfile($profileId) === NULL) {
      $form_state->setErrorByName('options][profile', $this->t('The selected conversion profile does not exist.'));
    }

    // Require at least one output format to be selected.
    $outputValues = $optionsValues['outputs'] ?? [];
    $selectedOutputs = $outputValues
      |> array_filter(...)
      |> array_keys(...);
    if ($selectedOutputs === []) {
      $form_state->setErrorByName('options][outputs', $this->t('Please select at least one output format.'));
    }
  }

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

    $optionsValues = $form_state->getValue('options') ?? [];
    $clearExisting = (bool) ($optionsValues['clear_existing'] ?? FALSE);
    $force = (bool) ($optionsValues['force'] ?? FALSE);
    $profileId = $optionsValues['profile'] ?? $this->profileManager->getDefaultProfileId();

    // Check if already queued — before any destructive operations.
    if (!$force && $this->mediaOperations->isQueued($this->media, $this->fieldName)) {
      $this->messenger()->addWarning($this->t('Media "@title" is already queued for transcoding.', [
        '@title' => $this->media->label(),
      ]));
      $this->setPostSubmitRedirect($form_state);
      return;
    }

    // Clear existing outputs if requested (safe: queue check passed above).
    if ($clearExisting) {
      $this->mediaOperations->clearOutputs($this->media, TRUE, $this->fieldName);
      $this->messenger()->addStatus($this->t('Existing outputs cleared.'));
    }

    // Collect selected outputs.
    $outputValues = $optionsValues['outputs'] ?? [];
    $selectedOutputs = $outputValues
      |> array_filter(...)
      |> array_keys(...);

    // Build options array. Force uses 'reconvert' operation which deletes
    // existing outputs before encoding (same semantics as Drush --force).
    $options = [
      'selected_outputs' => $selectedOutputs,
      'operation' => $force ? 'reconvert' : 'convert',
    ];

    // When force is set, remove any existing queue entry first so re-queue
    // succeeds.
    if ($force && $this->mediaOperations->isQueued($this->media, $this->fieldName)) {
      $this->mediaOperations->cancelQueuedTranscoding($this->media, $this->fieldName);
    }

    // Queue for transcoding with explicit field scoping.
    $success = $this->mediaOperations->queueForTranscode($this->media, $profileId, $options, $this->fieldName);

    if ($success) {
      $this->messenger()->addStatus($this->t('Media "@title" has been queued for transcoding.', [
        '@title' => $this->media->label(),
      ]));
    }
    else {
      $this->messenger()->addError($this->t('Failed to queue media for transcoding. Please check the logs for details.'));
    }

    $this->setPostSubmitRedirect($form_state);
  }

  /**
   * Submit handler to cancel a queued transcoding.
   *
   * @param array<string, mixed> $form
   *   The form array.
   * @param \Drupal\Core\Form\FormStateInterface $form_state
   *   The form state.
   */
  public function cancelQueuedTranscoding(array &$form, FormStateInterface $form_state): void {
    if ($this->media === NULL) {
      $this->messenger()->addError($this->t('Media not found.'));
      return;
    }

    $this->mediaOperations->cancelQueuedTranscoding($this->media, $this->fieldName);
    $this->messenger()->addStatus($this->t('Queued transcoding for "@title" has been cancelled.', [
      '@title' => $this->media->label(),
    ]));

    $routeOptions = $this->fieldName !== '' ? ['query' => ['field_name' => $this->fieldName]] : [];
    $form_state->setRedirect('ffmpeg_media_ui.transcode', ['media' => $this->media->id()], $routeOptions);
  }

  /**
   * Sets a post-submit redirect without forcing users into 403 pages.
   */
  protected function setPostSubmitRedirect(FormStateInterface $form_state): void {
    if ($this->media === NULL) {
      return;
    }

    $routeOptions = $this->fieldName !== '' ? ['query' => ['field_name' => $this->fieldName]] : [];

    if ($this->currentUser()->hasPermission('access ffmpeg media ui') && $this->media->access('view')) {
      $form_state->setRedirect('ffmpeg_media_ui.status', ['media' => $this->media->id()], $routeOptions);
      return;
    }

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

  /**
   * Formats video info for display.
   *
   * @param array{duration: float, width: int, height: int, codec: string, format: string, bitrate: int|null, framerate: float|null} $videoInfo
   *   Video information.
   *
   * @return list<\Drupal\Core\StringTranslation\TranslatableMarkup>
   *   Formatted items for item_list.
   */
  protected function formatVideoInfo(array $videoInfo): array {
    $items = [];

    if (isset($videoInfo['duration'])) {
      $items[] = $this->t('Duration: @duration', [
        '@duration' => gmdate('H:i:s', (int) $videoInfo['duration']),
      ]);
    }

    if (isset($videoInfo['width'], $videoInfo['height'])) {
      $items[] = $this->t('Resolution: @width × @height', [
        '@width' => $videoInfo['width'],
        '@height' => $videoInfo['height'],
      ]);
    }

    if (isset($videoInfo['codec'])) {
      $items[] = $this->t('Video Codec: @codec', ['@codec' => strtoupper($videoInfo['codec'])]);
    }

    if (isset($videoInfo['bitrate'])) {
      $items[] = $this->t('Bitrate: @bitrate Mbps', [
        '@bitrate' => round($videoInfo['bitrate'] / 1000000, 2),
      ]);
    }

    if (isset($videoInfo['framerate'])) {
      $items[] = $this->t('Frame Rate: @fps fps', ['@fps' => round($videoInfo['framerate'], 2)]);
    }

    return $items;
  }

}
