<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Commands;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\ffmpeg_media\ValueObject\ConversionCommandDependencies;
use Drupal\media\MediaInterface;
use Drush\Attributes as CLI;
use Drush\Commands\DrushCommands;

/**
 * Drush commands for FFmpeg Media video conversion operations.
 */
final class ConversionCommands extends DrushCommands {

  use CommandHelperTrait;

  /**
   * Constructs a ConversionCommands instance.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionCommandDependencies $deps
   *   Bundled conversion command dependencies.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   */
  public function __construct(
    protected readonly ConversionCommandDependencies $deps,
    protected readonly EntityTypeManagerInterface $entityTypeManager,
  ) {
    parent::__construct();
  }

  /**
   * Convert a single video by media ID.
   *
   * @param int $media_id
   *   The media entity ID to convert.
   * @param array $options
   *   Command options.
   *
   * @phpstan-param array{force: bool, profile: string|null} $options
   */
  #[CLI\Command(name: 'ffmpeg:convert', aliases: ['ffc'])]
  #[CLI\Argument(name: 'media_id', description: 'The media entity ID to convert.')]
  #[CLI\Option(name: 'force', description: 'Force reconversion even if already converted.')]
  #[CLI\Option(name: 'profile', description: 'Conversion profile ID to use.')]
  #[CLI\Usage(name: 'ffmpeg:convert 123', description: 'Convert video with media ID 123.')]
  #[CLI\Usage(name: 'ffmpeg:convert 123 --force', description: 'Reconvert video 123 even if already done.')]
  #[CLI\Usage(name: 'ffmpeg:convert 123 --profile=av1_opus_webm_mp4', description: 'Convert using specific profile.')]
  public function convertSingle(int $media_id, array $options = ['force' => FALSE, 'profile' => NULL]): void {
    $force = (bool) $options['force'];
    $profileId = $options['profile'] ?? NULL;

    $media = $this->loadMedia($media_id, $this->entityTypeManager, $this->deps->formatterSettingsResolver);
    if ($media === NULL) {
      return;
    }

    $status = $this->deps->videoConverter->getStatus($media);

    if ($status['is_converted'] && !$force) {
      $this->logger()?->warning('Media {id} is already converted. Use --force to reconvert.', ['id' => $media_id]);
      return;
    }

    if ($status['is_converted'] && $force) {
      $reconvertCheck = $this->deps->videoConverter->canReconvert($media);
      if (!$reconvertCheck['can_reconvert']) {
        $this->logger()?->error('Cannot reconvert media {id}: {reason}', [
          'id' => $media_id,
          'reason' => $reconvertCheck['reason'] ?? 'Unknown reason',
        ]);
        return;
      }

      $this->logger()?->notice('Force reconverting media {id}...', ['id' => $media_id]);

      // Delete existing converted files.
      $deleted = $this->deps->videoConverter->deleteConvertedFiles($media);
      if (!empty($deleted)) {
        $this->logger()?->notice('Deleted existing files: {files}', [
          'files' => implode(', ', $deleted),
        ]);
      }

      // Reload media entity.
      $media = $this->loadMedia($media_id, $this->entityTypeManager, $this->deps->formatterSettingsResolver);
      if ($media === NULL) {
        return;
      }
    }
    else {
      $this->logger()?->notice('Starting conversion for media {id}...', ['id' => $media_id]);
    }

    if ($profileId !== NULL && !$this->deps->profileManager->profileExists($profileId)) {
      $this->logger()?->warning(dt('Profile "@id" not found, falling back to default profile.', ['@id' => $profileId]));
    }

    try {
      $result = $this->deps->videoConverter->convert($media, $profileId);

      if ($result->isSuccess()) {
        $this->logger()?->success('Video conversion completed for media {id} in {duration}s.', [
          'id' => $media_id,
          'duration' => round($result->duration, 2),
        ]);
        $this->io()->listing(array_map(
          fn($format, $path) => "{$format}: {$path}",
          array_keys($result->outputs),
          $result->outputs,
        ));
      }
      else {
        $this->logger()?->error('Video conversion failed for media {id}: {error}', [
          'id' => $media_id,
          'error' => $result->error ?? 'Unknown error',
        ]);
      }
    }
    catch (\Throwable $e) {
      $this->logger()?->error('Video conversion failed for media {id}: {error}', [
        'id' => $media_id,
        'error' => $e->getMessage(),
      ]);
    }
  }

  /**
   * Queue all unconverted videos for conversion.
   *
   * @param array $options
   *   Command options.
   *
   * @phpstan-param array{limit: int, force: bool, bundle: string|null} $options
   */
  #[CLI\Command(name: 'ffmpeg:convert-all', aliases: ['ffca'])]
  #[CLI\Option(name: 'limit', description: 'Maximum number of videos to queue (0 = no limit).')]
  #[CLI\Option(name: 'force', description: 'Force reconversion of already converted videos.')]
  #[CLI\Option(name: 'bundle', description: 'Only process media of this bundle type.')]
  #[CLI\Usage(name: 'ffmpeg:convert-all', description: 'Queue all unconverted videos.')]
  #[CLI\Usage(name: 'ffmpeg:convert-all --limit=10', description: 'Queue up to 10 unconverted videos.')]
  #[CLI\Usage(name: 'ffmpeg:convert-all --force', description: 'Reconvert ALL videos including already converted.')]
  #[CLI\Usage(name: 'ffmpeg:convert-all --bundle=video', description: 'Only queue videos from the "video" bundle.')]
  public function convertAll(array $options = ['limit' => 0, 'force' => FALSE, 'bundle' => NULL]): void {
    $limit = (int) $options['limit'];
    $force = (bool) $options['force'];
    $bundle = $options['bundle'] ?? NULL;

    if ($force) {
      $this->io()->warning('Force mode enabled - will reconvert ALL videos including already converted ones.');
      if (!$this->io()->confirm('Continue?', FALSE)) {
        $this->logger()?->notice('Aborted.');
        return;
      }
    }

    // Get media IDs to process.
    $mediaIds = $this->findEligibleMedia($limit, $bundle, !$force, $this->entityTypeManager, $this->deps->formatterSettingsResolver, $this->deps->outputStorage);

    if (empty($mediaIds)) {
      $this->logger()?->notice('No video media entities found to process.');
      return;
    }

    $stats = [
      'queued' => 0,
      'converted' => 0,
      'already_queued' => 0,
      'no_source' => 0,
      'failed' => 0,
    ];

    $progressBar = $this->io()->createProgressBar(count($mediaIds));
    $progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %message%');
    $progressBar->setMessage('Queueing videos...');
    $progressBar->start();

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

    foreach ($mediaIds as $mediaId) {
      $progressBar->setMessage("Processing media {$mediaId}...");

      $media = $mediaEntities[$mediaId] ?? NULL;
      if (!$media instanceof MediaInterface) {
        $stats['failed']++;
        $progressBar->advance();
        continue;
      }

      // Check current status.
      $status = $this->deps->videoConverter->getStatus($media);

      if (!$status['has_source']) {
        $stats['no_source']++;
        $progressBar->advance();
        continue;
      }

      if ($status['is_converted'] && !$force) {
        $stats['converted']++;
        $progressBar->advance();
        continue;
      }

      if ($status['is_queued']) {
        $stats['already_queued']++;
        $progressBar->advance();
        continue;
      }

      // Queue per field context to support multi-field media types.
      $operation = $force && $status['is_converted'] ? 'reconvert' : 'convert';
      $fieldQueued = $this->queuePerField($media, $operation);
      $stats['queued'] += $fieldQueued['queued'];
      $stats['already_queued'] += $fieldQueued['skipped'];

      $progressBar->advance();
    }

    $progressBar->setMessage('Done!');
    $progressBar->finish();
    $this->io()->newLine(2);

    $this->logger()?->success('Queued {queued} videos. Skipped: {converted} converted, {already} in queue, {missing} no source, {failed} errors.', [
      'queued' => $stats['queued'],
      'converted' => $stats['converted'],
      'already' => $stats['already_queued'],
      'missing' => $stats['no_source'],
      'failed' => $stats['failed'],
    ]);
  }

  /**
   * Reconvert all already converted videos.
   *
   * @param array $options
   *   Command options.
   *
   * @phpstan-param array{limit: int, bundle: string|null} $options
   */
  #[CLI\Command(name: 'ffmpeg:reconvert', aliases: ['ffrc'])]
  #[CLI\Option(name: 'limit', description: 'Maximum number of videos to queue (0 = no limit).')]
  #[CLI\Option(name: 'bundle', description: 'Only process media of this bundle type.')]
  #[CLI\Usage(name: 'ffmpeg:reconvert', description: 'Queue all converted videos for reconversion.')]
  #[CLI\Usage(name: 'ffmpeg:reconvert --limit=10', description: 'Queue up to 10 converted videos.')]
  public function reconvert(array $options = ['limit' => 0, 'bundle' => NULL]): void {
    $limit = (int) $options['limit'];
    $bundle = $options['bundle'] ?? NULL;

    // Find converted videos.
    $mediaIds = $this->findConvertedMedia($limit, $bundle, $this->entityTypeManager, $this->deps->formatterSettingsResolver, $this->deps->outputStorage);

    if (empty($mediaIds)) {
      $this->logger()?->notice('No converted videos found.');
      return;
    }

    $count = count($mediaIds);
    $this->io()->warning("Found {$count} converted videos to reconvert.");
    $this->io()->text('This will queue them for reconversion with the current settings.');

    if (!$this->io()->confirm('Continue?', FALSE)) {
      $this->logger()?->notice('Aborted.');
      return;
    }

    $queued = 0;
    $skipped = 0;

    $progressBar = $this->io()->createProgressBar($count);
    $progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%% %message%');
    $progressBar->start();

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

    foreach ($mediaIds as $mediaId) {
      $progressBar->setMessage("Processing media {$mediaId}...");

      $media = $mediaEntities[$mediaId] ?? NULL;
      if (!$media instanceof MediaInterface) {
        $skipped++;
        $progressBar->advance();
        continue;
      }

      $fieldQueued = $this->queuePerField($media, 'reconvert');
      $queued += $fieldQueued['queued'];
      $skipped += $fieldQueued['skipped'];

      $progressBar->advance();
    }

    $progressBar->setMessage('Done!');
    $progressBar->finish();
    $this->io()->newLine(2);

    $this->logger()?->success('Queued {queued} videos for reconversion. Skipped: {skipped}.', [
      'queued' => $queued,
      'skipped' => $skipped,
    ]);

    $this->io()->note('Run "drush ffmpeg:process" to start processing the queue.');
  }

  /**
   * Requeue unconverted videos that are not currently queued.
   *
   * @param array $options
   *   Command options.
   *
   * @phpstan-param array{limit: int, bundle: string|null} $options
   */
  #[CLI\Command(name: 'ffmpeg:requeue-missing', aliases: ['ffrm'])]
  #[CLI\Option(name: 'limit', description: 'Maximum number of videos to queue (0 = no limit).')]
  #[CLI\Option(name: 'bundle', description: 'Only process media of this bundle type.')]
  #[CLI\Usage(name: 'ffmpeg:requeue-missing', description: 'Queue missing conversions not currently queued.')]
  #[CLI\Usage(name: 'ffmpeg:requeue-missing --limit=50', description: 'Queue up to 50 missing conversions.')]
  public function requeueMissing(array $options = ['limit' => 0, 'bundle' => NULL]): void {
    $limit = (int) $options['limit'];
    $bundle = $options['bundle'] ?? NULL;

    // Find unconverted videos not in queue.
    $mediaIds = $this->findEligibleMedia($limit, $bundle, TRUE, $this->entityTypeManager, $this->deps->formatterSettingsResolver, $this->deps->outputStorage);

    if (empty($mediaIds)) {
      $this->logger()?->notice('No videos found that need queueing.');
      return;
    }

    $stats = [
      'queued' => 0,
      'already_queued' => 0,
      'converted' => 0,
      'no_source' => 0,
    ];

    $progressBar = $this->io()->createProgressBar(count($mediaIds));
    $progressBar->setFormat(' %current%/%max% [%bar%] %percent:3s%%');
    $progressBar->start();

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

    foreach ($mediaIds as $mediaId) {
      $media = $mediaEntities[$mediaId] ?? NULL;
      if (!$media instanceof MediaInterface) {
        $progressBar->advance();
        continue;
      }

      $status = $this->deps->videoConverter->getStatus($media);

      if (!$status['has_source']) {
        $stats['no_source']++;
      }
      elseif ($status['is_converted']) {
        $stats['converted']++;
      }
      elseif ($status['is_queued']) {
        $stats['already_queued']++;
      }
      else {
        $fieldQueued = $this->queuePerField($media, 'requeue');
        $stats['queued'] += $fieldQueued['queued'];
        $stats['already_queued'] += $fieldQueued['skipped'];
      }

      $progressBar->advance();
    }

    $progressBar->finish();
    $this->io()->newLine(2);

    $this->logger()?->success('Requeued {queued} videos. Skipped: {converted} converted, {already} in queue, {missing} no source.', [
      'queued' => $stats['queued'],
      'converted' => $stats['converted'],
      'already' => $stats['already_queued'],
      'missing' => $stats['no_source'],
    ]);
  }

  /**
   * Queues a media entity per configured formatter field context.
   *
   * Resolves all FFmpeg formatter contexts for the media entity and queues
   * conversion for each field individually. This ensures multi-field media
   * types are correctly handled instead of silently skipped.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param string $operation
   *   Queue operation: 'convert', 'reconvert', or 'requeue'.
   *
   * @return array{queued: int, skipped: int}
   *   Counts of successfully queued and skipped fields.
   */
  private function queuePerField(MediaInterface $media, string $operation): array {
    $contexts = $this->deps->formatterSettingsResolver->getContextsForEntity($media);

    // Single-context or no-context: queue without explicit field (auto-detect).
    if (count($contexts) <= 1) {
      $fieldName = $contexts[0]->fieldName ?? '';
      $result = $this->deps->queueManager->queueForConversion($media, $operation, fieldName: $fieldName);
      return $result ? ['queued' => 1, 'skipped' => 0] : ['queued' => 0, 'skipped' => 1];
    }

    // Multi-context: queue each field explicitly.
    $queued = 0;
    $skipped = 0;
    foreach ($contexts as $context) {
      if ($this->deps->queueManager->queueForConversion($media, $operation, fieldName: $context->fieldName)) {
        $queued++;
      }
      else {
        $skipped++;
      }
    }

    return ['queued' => $queued, 'skipped' => $skipped];
  }

}
