<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Commands;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface;
use Drupal\ffmpeg_media\Service\OutputStorageServiceInterface;
use Drupal\ffmpeg_media\ValueObject\ConversionContext;
use Drupal\media\MediaInterface;

/**
 * Shared helper methods for FFmpeg Media Drush command classes.
 */
trait CommandHelperTrait {

  /**
   * Loads and validates a media entity.
   *
   * @param int $mediaId
   *   Media entity ID.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface $formatterSettingsResolver
   *   Formatter settings resolver service.
   *
   * @return \Drupal\media\MediaInterface|null
   *   Media entity or NULL if not found/invalid.
   */
  protected function loadMedia(int $mediaId, EntityTypeManagerInterface $entityTypeManager, FormatterSettingsResolverInterface $formatterSettingsResolver): ?MediaInterface {
    $media = $entityTypeManager->getStorage('media')->load($mediaId);

    if (!$media instanceof MediaInterface) {
      $this->logger()?->error('Media entity {id} not found.', ['id' => $mediaId]);
      return NULL;
    }

    // Check if this media type has a conversion context.
    $contexts = $formatterSettingsResolver->getContextsForEntity($media);
    if (empty($contexts)) {
      $this->logger()?->error('Media entity {id} (bundle: {bundle}) has no video conversion mapping.', [
        'id' => $mediaId,
        'bundle' => $media->bundle(),
      ]);
      return NULL;
    }

    return $media;
  }

  /**
   * Finds eligible media entities for conversion.
   *
   * @param int $limit
   *   Maximum number of results (0 = no limit).
   * @param string|null $bundle
   *   Restrict to specific bundle.
   * @param bool $excludeConverted
   *   Whether to exclude already converted media.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface $formatterSettingsResolver
   *   Formatter settings resolver service.
   * @param \Drupal\ffmpeg_media\Service\OutputStorageServiceInterface $outputStorage
   *   Output storage service.
   *
   * @phpstan-return array<int|string, int|string>
   */
  protected function findEligibleMedia(int $limit, ?string $bundle, bool $excludeConverted, EntityTypeManagerInterface $entityTypeManager, FormatterSettingsResolverInterface $formatterSettingsResolver, OutputStorageServiceInterface $outputStorage): array {
    $contexts = $formatterSettingsResolver->getAllContexts();
    $mediaIds = [];

    foreach ($contexts as $context) {
      if ($bundle !== NULL && $context->bundle !== $bundle) {
        continue;
      }

      // Drush: admin context, access checks not needed.
      $query = $entityTypeManager->getStorage('media')->getQuery()
        ->condition('bundle', $context->bundle)
        ->accessCheck(FALSE);

      // Exclude converted if requested.
      if ($excludeConverted) {
        $convertedIds = $outputStorage->getConvertedMediaIds();
        if (!empty($convertedIds)) {
          $query->condition('mid', $convertedIds, 'NOT IN');
        }
      }

      if ($limit > 0) {
        $remaining = $limit - count($mediaIds);
        if ($remaining > 0) {
          $query->range(0, $remaining);
        }
        else {
          break;
        }
      }

      $mediaIds = array_merge($mediaIds, array_values($query->execute()));

      if ($limit > 0 && count($mediaIds) >= $limit) {
        break;
      }
    }

    return array_slice($mediaIds, 0, $limit > 0 ? $limit : NULL);
  }

  /**
   * Finds converted media entities.
   *
   * @param int $limit
   *   Maximum number of results (0 = no limit).
   * @param string|null $bundle
   *   Restrict to specific bundle.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface $formatterSettingsResolver
   *   Formatter settings resolver service.
   * @param \Drupal\ffmpeg_media\Service\OutputStorageServiceInterface $outputStorage
   *   Output storage service.
   *
   * @phpstan-return array<int|string, int|string>
   */
  protected function findConvertedMedia(int $limit, ?string $bundle, EntityTypeManagerInterface $entityTypeManager, FormatterSettingsResolverInterface $formatterSettingsResolver, OutputStorageServiceInterface $outputStorage): array {
    $contexts = $formatterSettingsResolver->getAllContexts();
    $mediaIds = [];

    foreach ($contexts as $context) {
      if ($bundle !== NULL && $context->bundle !== $bundle) {
        continue;
      }

      // Drush: admin context, access checks not needed.
      $query = $entityTypeManager->getStorage('media')->getQuery()
        ->condition('bundle', $context->bundle)
        ->accessCheck(FALSE);

      // Find media with at least one output.
      $convertedIds = $outputStorage->getConvertedMediaIds();
      if (!empty($convertedIds)) {
        $query->condition('mid', $convertedIds, 'IN');
      }

      if ($limit > 0) {
        $remaining = $limit - count($mediaIds);
        if ($remaining > 0) {
          $query->range(0, $remaining);
        }
        else {
          break;
        }
      }

      $mediaIds = array_merge($mediaIds, array_values($query->execute()));

      if ($limit > 0 && count($mediaIds) >= $limit) {
        break;
      }
    }

    return array_slice($mediaIds, 0, $limit > 0 ? $limit : NULL);
  }

  /**
   * Finds field-scoped conversion targets for bulk operations.
   *
   * Returns a flat list of media ID + field name pairs, expanding each media
   * entity across all unique fields from the formatter contexts.
   *
   * @param int $limit
   *   Maximum number of media entities to query (0 = no limit).
   * @param string|null $bundle
   *   Restrict to specific bundle.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface $formatterSettingsResolver
   *   Formatter settings resolver service.
   *
   * @return array<int, array{media_id: int|string, field_name: string}>
   *   List of conversion target pairs.
   */
  protected function findConversionTargets(int $limit, ?string $bundle, EntityTypeManagerInterface $entityTypeManager, FormatterSettingsResolverInterface $formatterSettingsResolver): array {
    $contexts = $formatterSettingsResolver->getAllContexts();

    // Collect unique bundles and fields from contexts.
    $bundleFields = [];
    $bundles = [];
    foreach ($contexts as $context) {
      if ($bundle !== NULL && $context->bundle !== $bundle) {
        continue;
      }
      $bundles[$context->bundle] = $context->bundle;
      $bundleFields[$context->bundle][$context->fieldName] = $context->fieldName;
    }

    if ($bundles === []) {
      return [];
    }

    // Query media IDs for all matching bundles in a single query.
    $query = $entityTypeManager->getStorage('media')->getQuery()
      ->condition('bundle', array_values($bundles), 'IN')
      ->accessCheck(FALSE);

    if ($limit > 0) {
      $query->range(0, $limit);
    }

    $mediaIds = array_values($query->execute());
    if ($mediaIds === []) {
      return [];
    }

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

    // Expand each media entity only across the fields configured for its
    // actual bundle. This avoids invalid cross-bundle field combinations.
    $targets = [];
    foreach ($mediaIds as $mediaId) {
      $media = $mediaEntities[$mediaId] ?? NULL;
      if (!$media instanceof MediaInterface) {
        continue;
      }

      foreach ($bundleFields[$media->bundle()] ?? [] as $fieldName) {
        $targets[] = ['media_id' => $mediaId, 'field_name' => $fieldName];
      }
    }

    return $targets;
  }

  /**
   * Finds field-scoped conversion targets for already converted media.
   *
   * @param int $limit
   *   Maximum number of media entities to query (0 = no limit).
   * @param string|null $bundle
   *   Restrict to specific bundle.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface $formatterSettingsResolver
   *   Formatter settings resolver service.
   * @param \Drupal\ffmpeg_media\Service\OutputStorageServiceInterface $outputStorage
   *   Output storage service.
   *
   * @return array<int, array{media_id: int|string, field_name: string}>
   *   List of conversion target pairs.
   */
  protected function findConvertedConversionTargets(int $limit, ?string $bundle, EntityTypeManagerInterface $entityTypeManager, FormatterSettingsResolverInterface $formatterSettingsResolver, OutputStorageServiceInterface $outputStorage): array {
    $convertedIds = $outputStorage->getConvertedMediaIds();
    if ($convertedIds === []) {
      return [];
    }

    $contexts = $formatterSettingsResolver->getAllContexts();

    $bundleFields = [];
    $bundles = [];
    foreach ($contexts as $context) {
      if ($bundle !== NULL && $context->bundle !== $bundle) {
        continue;
      }
      $bundles[$context->bundle] = $context->bundle;
      $bundleFields[$context->bundle][$context->fieldName] = $context->fieldName;
    }

    if ($bundles === []) {
      return [];
    }

    $query = $entityTypeManager->getStorage('media')->getQuery()
      ->condition('bundle', array_values($bundles), 'IN')
      ->condition('mid', $convertedIds, 'IN')
      ->accessCheck(FALSE);

    if ($limit > 0) {
      $query->range(0, $limit);
    }

    $mediaIds = array_values($query->execute());
    if ($mediaIds === []) {
      return [];
    }

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

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

      foreach ($bundleFields[$media->bundle()] ?? [] as $fieldName) {
        $targets[] = ['media_id' => $mediaId, 'field_name' => $fieldName];
      }
    }

    return $targets;
  }

  /**
   * Resolves a single conversion context for a media entity.
   *
   * When a media entity has multiple FFmpeg formatter contexts (multiple
   * video fields), an explicit field name is required to disambiguate.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param \Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface $formatterSettingsResolver
   *   Formatter settings resolver service.
   * @param string $fieldName
   *   Explicit field name to select (empty for auto-detection).
   *
   * @return \Drupal\ffmpeg_media\ValueObject\ConversionContext|null
   *   The resolved context, or NULL if ambiguous or not found.
   */
  protected function resolveCommandContext(MediaInterface $media, FormatterSettingsResolverInterface $formatterSettingsResolver, string $fieldName = ''): ?ConversionContext {
    $contexts = $formatterSettingsResolver->getContextsForEntity($media);

    if ($contexts === []) {
      $this->logger()?->error('Media entity {id} has no FFmpeg formatter contexts.', [
        'id' => (int) $media->id(),
      ]);
      return NULL;
    }

    // If an explicit field was given, find its context.
    if ($fieldName !== '') {
      foreach ($contexts as $context) {
        if ($context->fieldName === $fieldName) {
          return $context;
        }
      }

      $this->logger()?->error('Media entity {id} has no FFmpeg formatter context for field {field}.', [
        'id' => (int) $media->id(),
        'field' => $fieldName,
      ]);
      return NULL;
    }

    // Single context: unambiguous.
    if (count($contexts) === 1) {
      return reset($contexts);
    }

    // Multiple contexts without an explicit field: ambiguous.
    $fieldNames = array_map(fn(ConversionContext $c) => $c->fieldName, $contexts);
    $this->logger()?->error('Media entity {id} has multiple FFmpeg formatter contexts ({fields}). Pass --field to select one explicitly.', [
      'id' => (int) $media->id(),
      'fields' => implode(', ', $fieldNames),
    ]);

    return NULL;
  }

  /**
   * Formats a duration in seconds to human-readable format.
   *
   * @param int $seconds
   *   Duration in seconds.
   *
   * @return string
   *   Formatted duration.
   */
  protected function formatDuration(int $seconds): string {
    if ($seconds < 60) {
      return $seconds . 's';
    }
    if ($seconds < 3600) {
      return (int) ($seconds / 60) . 'm ' . ($seconds % 60) . 's';
    }
    return (int) ($seconds / 3600) . 'h ' . (int) (($seconds % 3600) / 60) . 'm';
  }

  /**
   * Formats a bitrate to human-readable format.
   *
   * @param int $bitrate
   *   Bitrate in bits per second.
   *
   * @return string
   *   Formatted bitrate.
   */
  protected function formatBitrate(int $bitrate): string {
    if ($bitrate >= 1000000) {
      return round($bitrate / 1000000, 2) . ' Mbps';
    }
    if ($bitrate >= 1000) {
      return round($bitrate / 1000, 0) . ' kbps';
    }
    return $bitrate . ' bps';
  }

  /**
   * Formats a progress percentage as a visual bar.
   *
   * @param int $percent
   *   Progress percentage.
   *
   * @return string
   *   Formatted progress bar.
   */
  protected function formatProgressBar(int $percent): string {
    $filled = (int) ($percent / 5);
    $empty = 20 - $filled;
    return '[' . str_repeat('=', $filled) . str_repeat(' ', $empty) . '] ' . $percent . '%';
  }

  /**
   * Resolves a storage URI from a filesystem path where possible.
   *
   * @param string $path
   *   Filesystem path or stream wrapper URI.
   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
   *   File system service.
   *
   * @return string
   *   Resolved storage URI.
   */
  protected function resolveStorageUri(string $path, FileSystemInterface $fileSystem): string {
    if (str_contains($path, '://')) {
      return $path;
    }

    $candidates = [
      'public://' => $fileSystem->realpath('public://'),
      'private://' => $fileSystem->realpath('private://'),
    ];

    foreach ($candidates as $scheme => $root) {
      if ($root === FALSE || $root === NULL || $root === '') {
        continue;
      }
      $root = rtrim($root, DIRECTORY_SEPARATOR);
      if ($path === $root) {
        return $scheme;
      }
      $prefix = $root . DIRECTORY_SEPARATOR;
      if (str_starts_with($path, $prefix)) {
        $relative = substr($path, strlen($prefix));
        $relative = str_replace(DIRECTORY_SEPARATOR, '/', $relative);
        return $scheme . $relative;
      }
    }

    return $path;
  }

  /**
   * Resolves a local filesystem path from a stream wrapper URI.
   *
   * @param string $path
   *   Stream wrapper URI or local path.
   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
   *   File system service.
   *
   * @return string|null
   *   Local path or NULL if unresolvable.
   */
  protected function resolveLocalPath(string $path, FileSystemInterface $fileSystem): ?string {
    if (!str_contains($path, '://')) {
      return $path;
    }

    $directory = dirname($path);
    $realDir = $fileSystem->realpath($directory);
    if ($realDir === FALSE) {
      $fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY);
      $realDir = $fileSystem->realpath($directory);
    }

    if ($realDir === FALSE) {
      return NULL;
    }

    return $realDir . DIRECTORY_SEPARATOR . basename($path);
  }

}
