<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media_ui\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\ffmpeg_media\Enum\ConversionStatus;
use Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface;
use Drupal\ffmpeg_media\Service\QueueManagerInterface;
use Drupal\ffmpeg_media\Service\QueueMetadataRepositoryInterface;
use Drupal\ffmpeg_media\Service\SourceFileResolverInterface;
use Drupal\media\MediaInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;

/**
 * Controller endpoints for the visual queue editor.
 */
final class QueueController extends ControllerBase {

  /**
   * Default maximum number of queue items returned per list request.
   */
  private const int DEFAULT_LIST_LIMIT = 250;

  /**
   * Absolute hard cap for queue list response size.
   */
  private const int MAX_LIST_LIMIT = 1000;

  /**
   * Queue row scan batch size floor.
   */
  private const int LIST_SCAN_BATCH_SIZE = 100;

  /**
   * Constructs a QueueController object.
   */
  public function __construct(
    protected readonly Connection $database,
    EntityTypeManagerInterface $entityTypeManager,
    protected readonly QueueManagerInterface $queueManager,
    protected readonly QueueMetadataRepositoryInterface $queueMetadataRepository,
    protected readonly FormatterSettingsResolverInterface $formatterSettingsResolver,
    protected readonly SourceFileResolverInterface $sourceFileResolver,
    protected readonly LoggerInterface $logger,
  ) {
    $this->entityTypeManager = $entityTypeManager;
  }

  /**
   * Returns queue items for the visual queue editor.
   */
  public function queueList(Request $request): JsonResponse {
    $requestedLimit = $request->query->getInt('limit', self::DEFAULT_LIST_LIMIT);
    $limit = max(1, min(self::MAX_LIST_LIMIT, $requestedLimit));
    $offset = max(0, $request->query->getInt('offset', 0));

    $page = $this->loadVisibleQueuePage($offset, $limit);
    if ($page['total'] === 0) {
      return $this->jsonResponse([
        'success' => TRUE,
        'items' => [],
        'meta' => [
          'total' => 0,
          'offset' => $offset,
          'limit' => $limit,
          'returned' => 0,
          'has_more' => FALSE,
        ],
      ]);
    }

    return $this->jsonResponse([
      'success' => TRUE,
      'items' => $page['items'],
      'meta' => [
        'total' => $page['total'],
        'offset' => $offset,
        'limit' => $limit,
        'returned' => count($page['items']),
        'has_more' => ($offset + count($page['items'])) < $page['total'],
      ],
    ]);
  }

  /**
   * Saves a manually reordered queue order.
   */
  public function queueReorder(Request $request): JsonResponse {
    try {
      $data = json_decode($request->getContent(), TRUE, 512, JSON_THROW_ON_ERROR);
    }
    catch (\JsonException) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => 'Invalid JSON payload.',
      ], 400);
    }

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

    $orderKeys = array_values(array_unique(array_filter(
      array_map('strval', $rawOrder),
      static fn(string $key): bool => str_contains($key, ':') && ((int) explode(':', $key, 2)[0]) > 0,
    )));

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

    return $this->jsonResponse([
      'success' => TRUE,
      'saved' => $this->queueMetadataRepository->replaceManualOrder($orderKeys),
    ]);
  }

  /**
   * Performs queue item actions (remove, pause, resume, priority).
   */
  public function queueAction(Request $request): JsonResponse {
    try {
      $data = json_decode($request->getContent(), TRUE, 512, JSON_THROW_ON_ERROR);
    }
    catch (\JsonException) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => 'Invalid JSON payload.',
      ], 400);
    }

    $itemId = (string) ($data['itemId'] ?? '');
    $action = (string) ($data['action'] ?? '');

    $parts = explode(':', $itemId, 2);
    $mediaId = (int) $parts[0];
    $fieldName = $parts[1] ?? '';

    if ($mediaId <= 0 || $action === '') {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => 'Missing itemId or action.',
      ], 400);
    }

    $compositeKey = $mediaId . ':' . $fieldName;

    /** @var \Drupal\media\MediaInterface|null $media */
    $media = $this->entityTypeManager->getStorage('media')->load($mediaId);
    if (!$media instanceof MediaInterface) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => 'Media entity not found.',
      ], 404);
    }
    if (!$media->access('update')) {
      return $this->jsonResponse([
        'success' => FALSE,
        'error' => 'Access denied for this media item.',
      ], 403);
    }

    try {
      switch ($action) {
        case 'remove':
          $this->queueManager->removeFromQueue($mediaId, $fieldName);
          $message = $this->t('Queue item removed.');
          break;

        case 'pause':
          if (!$this->queueMetadataRepository->setPaused($compositeKey, TRUE)) {
            return $this->missingQueueItemResponse();
          }
          $message = $this->t('Queue item paused.');
          break;

        case 'resume':
          if (!$this->queueMetadataRepository->setPaused($compositeKey, FALSE)) {
            return $this->missingQueueItemResponse();
          }
          $message = $this->t('Queue item resumed.');
          break;

        case 'priority':
          $priority = max(0, min(2, (int) ($data['priority'] ?? 0)));
          if (!$this->queueMetadataRepository->setPriority($compositeKey, $priority)) {
            return $this->missingQueueItemResponse();
          }
          $message = $this->t('Queue priority updated.');
          break;

        default:
          return $this->jsonResponse([
            'success' => FALSE,
            'error' => $this->t('Unsupported action: @action', ['@action' => $action]),
          ], 400);
      }
    }
    catch (\Throwable $e) {
      $this->logger->error('Queue action @action failed for media @id (@field): @error', [
        '@action' => $action,
        '@id' => $mediaId,
        '@field' => $fieldName,
        '@error' => $e->getMessage(),
      ]);

      return $this->jsonResponse([
        'success' => FALSE,
        'error' => 'Queue action failed.',
      ], 500);
    }

    return $this->jsonResponse([
      'success' => TRUE,
      'message' => $message,
      'itemId' => $compositeKey,
      'action' => $action,
    ]);
  }

  /**
   * Loads and filters a visible queue page without materializing the whole UI.
   *
   * @return array{items: array<int, array<string, int|string>>, total: int}
   *   Visible page items and visible total count.
   */
  private function loadVisibleQueuePage(int $offset, int $limit): array {
    $batchSize = max(self::LIST_SCAN_BATCH_SIZE, $limit * 2);
    $rowOffset = 0;
    $visibleTotal = 0;
    $pageItems = [];

    do {
      $rows = $this->queueMetadataRepository->loadOrderedRows($rowOffset, $batchSize);
      if ($rows === []) {
        break;
      }

      $visibleBatch = $this->buildVisibleItemsFromRows($rows);
      $batchVisibleCount = count($visibleBatch);
      $batchVisibleStart = $visibleTotal;
      $visibleTotal += $batchVisibleCount;

      if ($batchVisibleCount > 0 && count($pageItems) < $limit && $offset < $visibleTotal) {
        $sliceOffset = max(0, $offset - $batchVisibleStart);
        if ($sliceOffset < $batchVisibleCount) {
          $pageItems = array_merge(
            $pageItems,
            array_slice($visibleBatch, $sliceOffset, $limit - count($pageItems)),
          );
        }
      }

      $rowOffset += count($rows);
    } while (count($rows) === $batchSize);

    return [
      'items' => $pageItems,
      'total' => $visibleTotal,
    ];
  }

  /**
   * Builds visible queue items from already ordered queue rows.
   *
   * @param array<int, object> $rows
   *   Ordered queue rows.
   *
   * @return array<int, array<string, int|string>>
   *   Visible queue items preserving row order.
   */
  private function buildVisibleItemsFromRows(array $rows): array {
    if ($rows === []) {
      return [];
    }

    $mediaIds = [];
    foreach ($rows as $row) {
      $mediaIds[(int) ($row->media_id ?? 0)] = (int) ($row->media_id ?? 0);
    }
    unset($mediaIds[0]);
    if ($mediaIds === []) {
      return [];
    }

    /** @var array<int, \Drupal\media\MediaInterface> $mediaEntities */
    $mediaEntities = $this->entityTypeManager->getStorage('media')->loadMultiple(array_values($mediaIds));

    $progressRows = $this->database->select('ffmpeg_media_progress', 'p')
      ->fields('p', ['media_id', 'field_name', 'status', 'progress_percent', 'updated'])
      ->condition('media_id', array_values($mediaIds), 'IN')
      ->execute()
      ?->fetchAll() ?? [];

    $progressByKey = [];
    foreach ($progressRows as $progressRow) {
      $progressByKey[((int) $progressRow->media_id) . ':' . ((string) ($progressRow->field_name ?? ''))] = $progressRow;
    }

    $contextMap = [];
    $fileIds = [];
    foreach ($mediaEntities as $mediaId => $media) {
      $contexts = $this->formatterSettingsResolver->getContextsForEntity($media);
      $context = $contexts[0] ?? NULL;
      if ($context === NULL) {
        continue;
      }
      $contextMap[$mediaId] = $context;
      $field = $media->get($context->fieldName);
      /** @var \Drupal\file\Plugin\Field\FieldType\FileItem|null $firstItem */
      $firstItem = $field->isEmpty() ? NULL : $field->first();
      $fileId = $firstItem?->target_id ?? NULL;
      if ($fileId !== NULL) {
        $fileIds[(int) $fileId] = (int) $fileId;
      }
    }
    if ($fileIds !== []) {
      $this->entityTypeManager->getStorage('file')->loadMultiple(array_values($fileIds));
    }

    $items = [];
    foreach ($rows as $row) {
      $mediaId = (int) ($row->media_id ?? 0);
      $fieldName = (string) ($row->field_name ?? '');
      $itemId = $mediaId . ':' . $fieldName;
      $media = $mediaEntities[$mediaId] ?? NULL;
      if (!$media instanceof MediaInterface || !$media->access('view')) {
        continue;
      }

      $name = (string) $media->label();
      if ($fieldName !== '') {
        $name .= ' (' . $fieldName . ')';
      }

      $status = ConversionStatus::Pending->value;
      $progress = 0;
      if (isset($progressByKey[$itemId])) {
        $progressRow = $progressByKey[$itemId];
        $status = (string) ($progressRow->status ?? ConversionStatus::Pending->value);
        $progress = (int) ($progressRow->progress_percent ?? 0);
      }

      $displayStatus = ((int) ($row->paused ?? 0)) === 1
        ? 'paused'
        : ($status === ConversionStatus::Converting->value ? 'processing' : $status);

      $filesize = 0;
      $sourceFieldName = $fieldName !== '' ? $fieldName : ($contextMap[$mediaId]?->fieldName ?? '');
      if ($sourceFieldName !== '') {
        $sourceFile = $this->sourceFileResolver->getSourceFile($media, $sourceFieldName);
        if ($sourceFile !== NULL) {
          $filesize = (int) $sourceFile->getSize();
        }
      }

      $items[] = [
        'id' => $itemId,
        'media_id' => $mediaId,
        'field_name' => $fieldName,
        'name' => $name,
        'status' => $displayStatus,
        'progress' => max(0, min(100, $progress)),
        'priority' => max(0, min(2, (int) ($row->priority ?? 0))),
        'created' => (int) ($row->queued_at ?? 0),
        'filesize' => $filesize,
        'profile' => (string) ($row->profile_id ?? 'av1_opus_webm_mp4'),
        'position' => (int) ($row->manual_order ?? QueueMetadataRepositoryInterface::DEFAULT_MANUAL_ORDER),
      ];
    }

    return $items;
  }

  /**
   * Builds a consistent stale-item response for queue actions.
   */
  private function missingQueueItemResponse(): JsonResponse {
    return $this->jsonResponse([
      'success' => FALSE,
      'error' => 'Queue item no longer exists.',
    ], 404);
  }

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

}
