<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media_ui\Service;

use Drupal\Component\Utility\Html;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Session\AccountProxyInterface;
use Drupal\Core\StringTranslation\ByteSizeMarkup;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\ffmpeg_media\Enum\OutputType;
use Drupal\ffmpeg_media\Service\ErrorCategorizationServiceInterface;
use Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface;
use Drupal\ffmpeg_media\Service\OutputStorageServiceInterface;
use Drupal\ffmpeg_media\Service\ProgressLabelServiceInterface;
use Drupal\file\FileInterface;
use Drupal\media\MediaInterface;

/**
 * Builds render arrays for the media status detail page.
 *
 * @internal
 */
final class MediaStatusPageBuilder implements MediaStatusPageBuilderInterface {

  use StringTranslationTrait;

  /**
   * Constructs a MediaStatusPageBuilder object.
   *
   * @param \Drupal\ffmpeg_media\Service\ErrorCategorizationServiceInterface $errorCategorization
   *   Error categorization service.
   * @param \Drupal\ffmpeg_media\Service\ProgressLabelServiceInterface $progressLabel
   *   Progress label service.
   * @param \Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface $formatterSettingsResolver
   *   Formatter settings resolver service.
   * @param \Drupal\ffmpeg_media\Service\OutputStorageServiceInterface $outputStorage
   *   Output storage service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   *   Module handler service.
   * @param \Drupal\Core\Access\AccessManagerInterface $accessManager
   *   Route access manager.
   * @param \Drupal\Core\Session\AccountProxyInterface $currentUser
   *   Current user service.
   */
  public function __construct(
    protected readonly ErrorCategorizationServiceInterface $errorCategorization,
    protected readonly ProgressLabelServiceInterface $progressLabel,
    protected readonly FormatterSettingsResolverInterface $formatterSettingsResolver,
    protected readonly OutputStorageServiceInterface $outputStorage,
    protected readonly ModuleHandlerInterface $moduleHandler,
    protected readonly AccessManagerInterface $accessManager,
    protected readonly AccountProxyInterface $currentUser,
  ) {}

  /**
   * {@inheritdoc}
   *
   * @phpstan-return array<string, mixed>|null
   */
  #[\Override]
  public function buildVideoPreview(MediaInterface $media, string $fieldName = ''): ?array {
    $sourceUrl = $this->getSourceFileUrl($media, $fieldName);
    if ($sourceUrl === NULL) {
      return NULL;
    }

    $posterUrl = $this->getPosterUrl($media, $fieldName);
    $dialogId = 'ffmpeg-video-dialog-' . $media->id();

    // Use a native button so keyboard activation and focus semantics are
    // handled by the browser instead of emulated in JavaScript.
    $thumbnail = [
      '#type' => 'html_tag',
      '#tag' => 'button',
      '#value' => NULL,
      '#attributes' => [
        'class' => ['ffmpeg-video-thumbnail'],
        'type' => 'button',
        'data-dialog-target' => $dialogId,
        'aria-haspopup' => 'dialog',
        'aria-controls' => $dialogId,
        'aria-expanded' => 'false',
        'aria-label' => $this->t('Play video preview'),
        'title' => $this->t('Play video preview'),
      ],
    ];

    // Thumbnail image: poster frame or metadata-only video.
    if ($posterUrl !== NULL) {
      $thumbnail['image'] = [
        '#type' => 'html_tag',
        '#tag' => 'img',
        '#attributes' => [
          'src' => $posterUrl,
          'alt' => '',
          'class' => ['ffmpeg-video-thumbnail__image'],
          'loading' => 'lazy',
        ],
      ];
    }
    else {
      $thumbnail['image'] = [
        '#type' => 'html_tag',
        '#tag' => 'video',
        '#value' => '',
        '#attributes' => [
          'src' => $sourceUrl,
          'preload' => 'metadata',
          'muted' => TRUE,
          'aria-hidden' => 'true',
          'class' => ['ffmpeg-video-thumbnail__image'],
        ],
      ];
    }

    // Play icon overlay (SVG).
    $thumbnail['play_icon'] = [
      '#type' => 'html_tag',
      '#tag' => 'span',
      '#attributes' => [
        'class' => ['ffmpeg-video-thumbnail__play-icon'],
        'aria-hidden' => 'true',
      ],
      '#value' => '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none"><circle cx="32" cy="32" r="30" fill="rgba(0,0,0,0.55)" stroke="#fff" stroke-width="2"/><polygon points="26,20 26,44 46,32" fill="#fff"/></svg>',
    ];

    // Dialog video attributes.
    $dialogVideoAttributes = [
      'class' => ['ffmpeg-video-dialog__video'],
      'controls' => TRUE,
      'preload' => 'none',
      'data-src' => $sourceUrl,
    ];
    if ($posterUrl !== NULL) {
      $dialogVideoAttributes['poster'] = $posterUrl;
    }

    // Native <dialog> modal with full-size player.
    $dialogTitleId = $dialogId . '-title';
    $dialog = [
      '#type' => 'html_tag',
      '#tag' => 'dialog',
      '#attributes' => [
        'id' => $dialogId,
        'class' => ['ffmpeg-video-dialog'],
        'aria-labelledby' => $dialogTitleId,
        'aria-modal' => 'true',
      ],
      'content_wrapper' => [
        '#type' => 'container',
        '#attributes' => ['class' => ['ffmpeg-video-dialog__content']],
        'title' => [
          '#type' => 'html_tag',
          '#tag' => 'h3',
          '#value' => $this->t('Video Player'),
          '#attributes' => [
            'id' => $dialogTitleId,
            'class' => ['visually-hidden'],
          ],
        ],
        'close_button' => [
          '#type' => 'html_tag',
          '#tag' => 'button',
          '#value' => '&#x2715;',
          '#attributes' => [
            'class' => ['ffmpeg-video-dialog__close'],
            'type' => 'button',
            'aria-label' => $this->t('Close video player'),
          ],
        ],
        'player' => [
          '#type' => 'html_tag',
          '#tag' => 'video',
          '#value' => '',
          '#attributes' => $dialogVideoAttributes,
        ],
      ],
    ];

    return [
      '#type' => 'container',
      '#attributes' => ['class' => ['ffmpeg-source-preview']],
      'thumbnail' => $thumbnail,
      'dialog' => $dialog,
    ];
  }

  /**
   * {@inheritdoc}
   *
   * @phpstan-param array<string, mixed> $status
   * @phpstan-return array<string, mixed>
   */
  #[\Override]
  public function buildProgressSection(array $status): array {
    // Humanize progress message.
    $humanizedMessage = $status['message'];
    if ($status['message'] !== NULL && $status['message'] !== '') {
      $humanizedMessage = $this->progressLabel->humanize(
        $status['message'],
        NULL,
        0,
        0,
      );
    }

    // ETA calculation.
    $eta = NULL;
    if ($status['status'] === 'processing' && isset($status['started'])) {
      $eta = $this->progressLabel->getEstimatedCompletion(
        (int) $status['started'],
        (int) $status['progress'],
      );
    }

    $progressMessage = $humanizedMessage;
    if ($eta !== NULL) {
      $progressMessage = ($progressMessage ?? '') . ' — ' . $eta;
    }

    return [
      '#type' => 'container',
      '#attributes' => [
        'class' => [
          'ffmpeg-media-dashboard__section',
          'ffmpeg-status-section',
          'ffmpeg-status-section--progress',
          'ffmpeg-status-section--status-' . $status['status'],
        ],
      ],
      'heading' => [
        '#type' => 'html_tag',
        '#tag' => 'h3',
        '#value' => $this->t('Conversion Progress'),
      ],
      'bar' => [
        '#theme' => 'ffmpeg_media_progress_bar',
        '#progress' => $status['progress'],
        '#status' => $status['status'],
        '#message' => $progressMessage,
        '#animated' => $status['status'] === 'processing',
        '#attached' => ['library' => ['ffmpeg_media_ui/progress-bar']],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   *
   * @phpstan-param array<string, mixed> $status
   * @phpstan-return array<string, mixed>|null
   */
  #[\Override]
  public function buildErrorDetails(array $status): ?array {
    if ($status['status'] !== 'failed' || empty($status['error'])) {
      return NULL;
    }

    $errorCategory = $this->errorCategorization->categorize($status['error']);

    return [
      '#type' => 'details',
      '#title' => $this->t('Error Details'),
      '#open' => TRUE,
      '#attributes' => ['class' => ['messages', 'messages--error']],
      'category' => [
        '#type' => 'html_tag',
        '#tag' => 'p',
        '#value' => '<strong>' . Html::escape((string) $errorCategory->label) . '</strong>',
      ],
      'message' => [
        '#type' => 'html_tag',
        '#tag' => 'p',
        '#value' => htmlspecialchars($status['error']),
      ],
      'suggestion' => [
        '#type' => 'html_tag',
        '#tag' => 'p',
        '#value' => $this->t('Suggestion: @suggestion', [
          '@suggestion' => $errorCategory->suggestion,
        ]),
        '#attributes' => ['class' => ['ffmpeg-error-suggestion']],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   *
   * @phpstan-param array<string, array<string, mixed>> $outputs
   * @phpstan-return array<string, mixed>|null
   */
  #[\Override]
  public function buildOutputsSection(array $outputs): ?array {
    // Exclude transcript: it has its own dedicated section.
    $videoOutputs = $outputs;
    unset($videoOutputs[OutputType::Transcript->value]);

    if ($videoOutputs === []) {
      return NULL;
    }

    return [
      '#type' => 'container',
      '#attributes' => [
        'class' => [
          'ffmpeg-media-dashboard__section',
          'ffmpeg-status-section',
          'ffmpeg-status-section--outputs',
        ],
      ],
      'heading' => [
        '#type' => 'html_tag',
        '#tag' => 'h3',
        '#value' => $this->t('Outputs'),
      ],
      'list' => [
        '#theme' => 'ffmpeg_media_output_list',
        '#outputs' => $videoOutputs,
        '#show_details' => TRUE,
        '#attached' => ['library' => ['ffmpeg_media_ui/output-list']],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   *
   * @phpstan-return array<string, mixed>|null
   */
  #[\Override]
  public function buildTranscriptSection(MediaInterface $media, string $fieldName = ''): ?array {
    $context = $fieldName !== ''
      ? $this->formatterSettingsResolver->getContextForField($media, $fieldName)
      : $this->formatterSettingsResolver->getPreferredContextForEntity($media);
    if ($context === NULL) {
      return NULL;
    }

    $transcriptFile = $this->outputStorage->getOutput((int) $media->id(), $context->fieldName, OutputType::Transcript->value);
    if ($transcriptFile === NULL) {
      // No transcript yet; show hint if Whisper is available.
      if ($this->canGenerateWhisperTranscript()) {
        return [
          '#type' => 'container',
          '#attributes' => [
            'class' => [
              'ffmpeg-media-dashboard__section',
              'ffmpeg-status-section',
              'ffmpeg-status-section--transcript',
            ],
          ],
          'heading' => [
            '#type' => 'html_tag',
            '#tag' => 'h3',
            '#value' => $this->t('Transcript'),
          ],
          'empty' => [
            '#type' => 'html_tag',
            '#tag' => 'p',
            '#value' => $this->t('No transcript generated yet. Use the <em>Generate Transcript</em> button to create subtitles using Whisper AI.'),
            '#attributes' => ['class' => ['ffmpeg-transcript-empty']],
          ],
        ];
      }

      return NULL;
    }

    $fileUrl = $transcriptFile->createFileUrl(FALSE);
    $fileName = $transcriptFile->getFilename();
    $fileSize = ByteSizeMarkup::create($transcriptFile->getSize() ?? 0);

    return [
      '#type' => 'container',
      '#attributes' => [
        'class' => [
          'ffmpeg-media-dashboard__section',
          'ffmpeg-status-section',
          'ffmpeg-status-section--transcript',
        ],
      ],
      'heading' => [
        '#type' => 'html_tag',
        '#tag' => 'h3',
        '#value' => $this->t('Transcript'),
      ],
      'info' => [
        '#theme' => 'item_list',
        '#items' => [
          $this->t('File: <a href=":url">@name</a>', [
            ':url' => $fileUrl,
            '@name' => $fileName,
          ]),
          $this->t('Size: @size', ['@size' => $fileSize]),
        ],
      ],
    ];
  }

  /**
   * {@inheritdoc}
   *
   * @phpstan-param array<string, mixed> $videoInfo
   * @phpstan-return array<string, mixed>
   */
  #[\Override]
  public function buildVideoInfoCard(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 x @height', [
        '@width' => $videoInfo['width'],
        '@height' => $videoInfo['height'],
      ]);
    }

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

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

    return [
      '#type' => 'details',
      '#title' => $this->t('Video Information'),
      '#open' => TRUE,
      '#attributes' => ['class' => ['ffmpeg-video-info']],
      'list' => [
        '#theme' => 'item_list',
        '#items' => $items,
      ],
    ];
  }

  /**
   * {@inheritdoc}
   *
   * @phpstan-return array<string, mixed>
   */
  #[\Override]
  public function buildActionButtons(MediaInterface $media, array $status, string $fieldName = ''): array {
    $actions = [
      '#type' => 'container',
      '#attributes' => ['class' => ['ffmpeg-actions']],
    ];
    $parameters = ['media' => $media->id()];
    $fieldRouteOptions = $fieldName !== '' ? ['query' => ['field_name' => $fieldName]] : [];

    if ($status['status'] !== 'processing') {
      if ($this->isRouteAccessible('ffmpeg_media_ui.transcode', $parameters)) {
        $actions['transcode'] = [
          '#type' => 'link',
          '#title' => $this->t('Transcode'),
          '#url' => Url::fromRoute('ffmpeg_media_ui.transcode', $parameters, $fieldRouteOptions),
          '#attributes' => ['class' => ['button', 'button--primary']],
        ];
      }

      if ($this->isRouteAccessible('ffmpeg_media_ui.poster_select', $parameters)) {
        $actions['poster'] = [
          '#type' => 'link',
          '#title' => $this->t('Select Poster'),
          '#url' => Url::fromRoute('ffmpeg_media_ui.poster_select', $parameters, $fieldRouteOptions),
          '#attributes' => ['class' => ['button']],
        ];
      }

      if ($this->canGenerateWhisperTranscript() && $this->isRouteAccessible('ffmpeg_media_whisper.transcribe', $parameters)) {
        $actions['transcribe'] = [
          '#type' => 'link',
          '#title' => $this->t('Generate Transcript'),
          '#url' => Url::fromRoute('ffmpeg_media_whisper.transcribe', $parameters, $fieldRouteOptions),
          '#attributes' => ['class' => ['button']],
        ];
      }
    }

    return $actions;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function getSourceFileUrl(MediaInterface $media, string $fieldName = ''): ?string {
    $sourceField = $fieldName !== '' ? $fieldName : ($media->getSource()->getConfiguration()['source_field'] ?? NULL);
    if ($sourceField === NULL || !$media->hasField($sourceField)) {
      return NULL;
    }

    $field = $media->get($sourceField);
    if ($field->isEmpty()) {
      return NULL;
    }

    $file = $field->entity;
    return $file instanceof FileInterface ? $file->createFileUrl(FALSE) : NULL;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function getPosterUrl(MediaInterface $media, string $fieldName = ''): ?string {
    // Check the output storage for a thumbnail.
    $mediaId = (int) $media->id();
    $sourceField = $fieldName !== '' ? $fieldName : ($media->getSource()->getConfiguration()['source_field'] ?? '');
    $thumbnailFile = $this->outputStorage->getOutput($mediaId, $sourceField, OutputType::Thumbnail->value);
    // Fallback: check legacy empty-field-name entry.
    if ($thumbnailFile === NULL && $sourceField !== '') {
      $thumbnailFile = $this->outputStorage->getOutput($mediaId, '', OutputType::Thumbnail->value);
    }
    if ($thumbnailFile instanceof FileInterface) {
      return $this->buildPosterUrl($thumbnailFile);
    }

    // Fallback to Drupal's built-in media thumbnail field.
    if ($media->hasField('thumbnail') && !$media->get('thumbnail')->isEmpty()) {
      $file = $media->get('thumbnail')->entity;
      if ($file instanceof FileInterface) {
        $uri = $file->getFileUri() ?? '';
        // Skip generic placeholder thumbnails.
        if ($uri !== '' && !str_contains($uri, 'generic')) {
          return $this->buildPosterUrl($file);
        }
      }
    }

    return NULL;
  }

  /**
   * Checks whether Whisper transcription is available.
   *
   * @return bool
   *   TRUE if the whisper module is installed.
   */
  #[\NoDiscard]
  protected function isWhisperAvailable(): bool {
    return $this->moduleHandler->moduleExists('ffmpeg_media_whisper');
  }

  /**
   * Checks whether current user can generate Whisper transcripts.
   *
   * @return bool
   *   TRUE when whisper module is enabled and permission is granted.
   */
  #[\NoDiscard]
  protected function canGenerateWhisperTranscript(): bool {
    return $this->isWhisperAvailable()
      && $this->currentUser->hasPermission('generate whisper transcripts');
  }

  /**
   * Checks whether the current user may access an action route.
   *
   * @param string $routeName
   *   Route name to check.
   * @param array<string, mixed> $parameters
   *   Route parameters.
   *
   * @return bool
   *   TRUE when the current user may access the route.
   */
  #[\NoDiscard]
  protected function isRouteAccessible(string $routeName, array $parameters): bool {
    return $this->accessManager->checkNamedRoute($routeName, $parameters, $this->currentUser);
  }

  /**
   * Builds a poster URL with cache-busting query parameter.
   *
   * @param \Drupal\file\FileInterface $file
   *   The poster file entity.
   *
   * @return string
   *   URL with version parameter to bust browser cache on poster change.
   */
  #[\NoDiscard]
  protected function buildPosterUrl(FileInterface $file): string {
    $url = $file->createFileUrl(FALSE);
    $changed = $file->getChangedTime();

    return $url . '?v=' . $changed;
  }

}
