<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media_ui\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface;
use Drupal\ffmpeg_media\Service\QueueManagerInterface;
use Drupal\ffmpeg_media\Trait\ErrorRedactionTrait;
use Drupal\ffmpeg_media_image\Service\TemporaryFileAccessManagerInterface;
use Drupal\ffmpeg_media_image\Service\ThumbnailGeneratorServiceInterface;
use Drupal\ffmpeg_media_ui\Service\MediaOperationsService;
use Drupal\media\MediaInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

/**
 * Controller for AJAX endpoints.
 */
final class AjaxController extends ControllerBase {

  use ErrorRedactionTrait;

  /**
   * Constructs an AjaxController object.
   *
   * @param \Drupal\ffmpeg_media_ui\Service\MediaOperationsService $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
   *   The entity type manager.
   * @param \Drupal\ffmpeg_media\Service\QueueManagerInterface $queueManager
   *   Queue manager service.
   * @param \Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface $formatterSettingsResolver
   *   Formatter settings resolver service.
   */
  public function __construct(
    protected readonly MediaOperationsService $mediaOperations,
    protected readonly ThumbnailGeneratorServiceInterface $thumbnailGenerator,
    protected readonly TemporaryFileAccessManagerInterface $temporaryFileAccessManager,
    EntityTypeManagerInterface $entityTypeManager,
    protected readonly QueueManagerInterface $queueManager,
    protected readonly FormatterSettingsResolverInterface $formatterSettingsResolver,
  ) {
    $this->entityTypeManager = $entityTypeManager;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  protected function getErrorRedactionAccount(): ?AccountInterface {
    return $this->currentUser();
  }

  /**
   * {@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(QueueManagerInterface::class),
      $container->get(FormatterSettingsResolverInterface::class),
    );
  }

  /**
   * Returns conversion progress for a media item.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request (reads optional ?field_name= query parameter).
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with progress data.
   */
  public function progress(MediaInterface $media, Request $request): JsonResponse {
    if (!$media->access('view')) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => $this->t('Access denied for this media item.'),
      ], 403);
    }

    $fieldName = (string) $request->query->get('field_name', '');
    $status = $this->mediaOperations->getStatus($media, $fieldName);

    return $this->jsonResponse([
      'success' => TRUE,
      'media_id' => $media->id(),
      'status' => $status['status'],
      'progress' => $status['progress'],
      'message' => $status['message'],
      'error' => $this->redactErrorString($status['error'] ?? NULL),
      'elapsed_seconds' => $status['elapsed_seconds'] ?? NULL,
    ]);
  }

  /**
   * Generates and returns thumbnail previews for poster selection.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request (reads optional ?field_name= query parameter).
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with thumbnail data.
   */
  public function thumbnails(MediaInterface $media, Request $request): JsonResponse {
    if (!$media->access('update')) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => $this->t('Access denied for this media item.'),
      ], 403);
    }

    $fieldName = (string) $request->query->get('field_name', '');
    $videoPath = $this->mediaOperations->getSourceFile($media, $fieldName);

    if ($videoPath === NULL) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => $this->t('Source file not found.'),
      ], 404);
    }

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

    if ($videoInfo === NULL || empty($videoInfo['duration'])) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => $this->t('Could not read video information.'),
      ], 400);
    }

    $thumbnails = $this->thumbnailGenerator->generateThumbnails(
      $videoPath,
      $videoInfo['duration'],
    );
    $this->registerTemporaryThumbnailAccess($thumbnails, (int) $media->id());

    if (empty($thumbnails)) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => $this->t('Failed to generate thumbnails.'),
      ], 500);
    }

    return $this->jsonResponse([
      'success' => TRUE,
      'media_id' => $media->id(),
      'duration' => $videoInfo['duration'],
      'thumbnails' => array_map(function ($thumb) {
        return [
          'url' => $thumb['url'],
          'timestamp' => $thumb['timestamp'],
          'time_formatted' => $thumb['time_formatted'],
          'index' => $thumb['index'],
        ];
      }, $thumbnails),
    ]);
  }

  /**
   * Registers generated private thumbnail URIs for entity-scoped access.
   *
   * @param array<int, array{uri?: string}> $thumbnails
   *   Generated thumbnail metadata.
   * @param int $mediaId
   *   Related media entity ID.
   */
  private function registerTemporaryThumbnailAccess(array $thumbnails, int $mediaId): void {
    foreach ($thumbnails as $thumbnail) {
      $uri = $thumbnail['uri'] ?? NULL;
      if (is_string($uri) && str_starts_with($uri, 'private://')) {
        $this->temporaryFileAccessManager->registerUri($uri, $mediaId);
      }
    }
  }

  /**
   * Handles batch operations on multiple media items.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The current request.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with batch operation results.
   */
  public function batchAction(Request $request): JsonResponse {
    $content = $request->getContent();

    try {
      $data = json_decode($content, TRUE, 512, JSON_THROW_ON_ERROR);
    }
    catch (\JsonException $e) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => $this->t('Invalid JSON in request body.'),
      ], 400);
    }

    if (!is_array($data) || empty($data['action'])) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => $this->t('Missing action.'),
      ], 400);
    }

    $action = (string) $data['action'];
    $allowedActions = ['transcode', 'requeue', 'delete-outputs'];
    if (!in_array($action, $allowedActions, TRUE)) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => $this->t('Unknown action: @action', ['@action' => $action]),
      ], 400);
    }

    if (!$this->currentUserCanPerformAction($action)) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => $this->t('Access denied for batch action "@action".', ['@action' => $action]),
      ], 403);
    }

    $rawMediaIds = $data['media_ids'] ?? $data['mediaIds'] ?? NULL;
    if (!is_array($rawMediaIds) || $rawMediaIds === []) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => $this->t('Missing media_ids.'),
      ], 400);
    }

    $mediaIds = array_values(array_unique(array_filter(
      array_map('intval', $rawMediaIds),
      static fn(int $id): bool => $id > 0,
    )));

    if ($mediaIds === []) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => $this->t('No valid media IDs provided.'),
      ], 400);
    }

    if (count($mediaIds) > 100) {
      return new JsonResponse(['success' => FALSE, 'error' => 'Maximum 100 items per batch request.'], 400);
    }

    $results = ['processed' => 0, 'errors' => []];

    $storage = $this->entityTypeManager->getStorage('media');
    $entities = $storage->loadMultiple($mediaIds);

    foreach ($mediaIds as $mediaId) {
      /** @var \Drupal\media\MediaInterface|null $media */
      $media = $entities[$mediaId] ?? NULL;
      if ($media === NULL) {
        $results['errors'][] = $this->t('Media @id not found.', ['@id' => $mediaId]);
        continue;
      }
      if (!$media->access('update')) {
        $results['errors'][] = $this->t('Media @id: access denied.', ['@id' => $mediaId]);
        continue;
      }
      if ($this->actionRequiresMapping($action) && $this->formatterSettingsResolver->getContextsForEntity($media) === []) {
        $results['errors'][] = $this->t('Media @id: no FFmpeg formatter mapping configured.', ['@id' => $mediaId]);
        continue;
      }

      try {
        if ($action === 'transcode') {
          $this->mediaOperations->queueForTranscode($media);
        }
        elseif ($action === 'requeue') {
          $this->mediaOperations->refresh($media);
        }
        else {
          $this->mediaOperations->clearOutputs($media);
        }
        $results['processed']++;
      }
      catch (\Throwable $e) {
        $this->getLogger('ffmpeg_media_ui')->error('Batch @action failed for media @id: @message', [
          '@action' => $action,
          '@id' => $mediaId,
          '@message' => $e->getMessage(),
        ]);
        $results['errors'][] = (string) $this->t('Media @id: operation failed.', ['@id' => $mediaId]);
      }
    }

    $hasErrors = $results['errors'] !== [];

    return $this->jsonResponse([
      'success' => !$hasErrors,
      'action' => $action,
      'results' => $results,
      'message' => $hasErrors
        ? $this->t('Batch operation completed with errors.')
        : $this->t('Batch operation completed successfully.'),
    ]);
  }

  /**
   * Returns whether the current user may perform a batch action.
   *
   * @param string $action
   *   The requested action.
   *
   * @return bool
   *   TRUE when the action is allowed for the current user.
   */
  #[\NoDiscard]
  protected function currentUserCanPerformAction(string $action): bool {
    return match ($action) {
      'transcode' => $this->currentUserCanTranscode(),
      'requeue', 'delete-outputs' => $this->currentUser()->hasPermission('manage media outputs'),
      default => FALSE,
    };
  }

  /**
   * Returns whether the current user can use transcode UI tools.
   */
  #[\NoDiscard]
  protected function currentUserCanTranscode(): bool {
    $account = $this->currentUser();

    return $account->hasPermission('transcode media')
      || $account->hasPermission('convert media')
      || $account->hasPermission('reconvert media');
  }

  /**
   * Returns whether the action requires an FFmpeg formatter mapping.
   *
   * @param string $action
   *   The requested action.
   *
   * @return bool
   *   TRUE when the action depends on a configured mapping.
   */
  #[\NoDiscard]
  protected function actionRequiresMapping(string $action): bool {
    return in_array($action, ['transcode', 'requeue'], TRUE);
  }

  /**
   * Creates a JSON response with no-cache headers.
   *
   * @param array<string, mixed> $data
   *   The response data.
   * @param int $status
   *   The HTTP status code.
   *
   * @return \Symfony\Component\HttpFoundation\JsonResponse
   *   JSON response with Cache-Control headers.
   */
  private function jsonResponse(array $data, int $status = 200): JsonResponse {
    return new JsonResponse($data, $status, [
      'Cache-Control' => 'no-cache, no-store, must-revalidate',
    ]);
  }

}
