<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media_ui\Service;

use Drupal\ffmpeg_media\Enum\ConversionStatus;
use Drupal\ffmpeg_media\ValueObject\ConversionProfile;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\file\FileInterface;
use Drupal\ffmpeg_media\Service\ConversionFileManagerInterface;
use Drupal\ffmpeg_media\Service\FFmpegExecutorInterface;
use Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface;
use Drupal\ffmpeg_media\Service\MediaArtifactManagerInterface;
use Drupal\ffmpeg_media\Service\OutputStorageServiceInterface;
use Drupal\ffmpeg_media\Service\ProgressTrackerInterface;
use Drupal\ffmpeg_media\Service\QueueManagerInterface;
use Drupal\ffmpeg_media\Service\VideoConverterInterface;
use Drupal\media\MediaInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
 * Service for media transcoding operations from UI.
 *
 * @internal
 */
final class MediaOperationsService implements MediaOperationsServiceInterface {

  /**
   * Constructs a MediaOperationsService object.
   *
   * @param \Drupal\ffmpeg_media\Service\VideoConverterInterface $videoConverter
   *   Video converter service.
   * @param \Drupal\ffmpeg_media\Service\QueueManagerInterface $queueManager
   *   Queue manager service.
   * @param \Drupal\ffmpeg_media\Service\ProgressTrackerInterface $progressTracker
   *   Progress tracker service.
   * @param \Drupal\ffmpeg_media\Service\FFmpegExecutorInterface $ffmpegExecutor
   *   FFmpeg executor service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
   *   File system service.
   * @param \Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface $formatterSettingsResolver
   *   Formatter settings resolver.
   * @param \Drupal\ffmpeg_media\Service\OutputStorageServiceInterface $outputStorage
   *   Output storage service.
   * @param \Drupal\ffmpeg_media\Service\ConversionFileManagerInterface $conversionFileManager
   *   Conversion file manager for file usage checks.
   * @param \Psr\Log\LoggerInterface $logger
   *   Logger channel.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   Config factory.
   * @param \Drupal\ffmpeg_media\Service\MediaArtifactManagerInterface|null $artifactManager
   *   Optional centralized artifact cleanup service.
   */
  public function __construct(
    protected readonly VideoConverterInterface $videoConverter,
    protected readonly QueueManagerInterface $queueManager,
    protected readonly ProgressTrackerInterface $progressTracker,
    protected readonly FFmpegExecutorInterface $ffmpegExecutor,
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly FileSystemInterface $fileSystem,
    protected readonly FormatterSettingsResolverInterface $formatterSettingsResolver,
    protected readonly OutputStorageServiceInterface $outputStorage,
    protected readonly ConversionFileManagerInterface $conversionFileManager,
    #[Autowire(service: 'logger.channel.ffmpeg_media')]
    protected readonly LoggerInterface $logger,
    #[Autowire(service: 'config.factory')]
    protected readonly ConfigFactoryInterface $configFactory,
    #[Autowire(service: MediaArtifactManagerInterface::class)]
    protected readonly ?MediaArtifactManagerInterface $artifactManager = NULL,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function queueForTranscode(
    MediaInterface $media,
    ?string $profileId = NULL,
    array $options = [],
    string $fieldName = '',
  ): bool {
    $resolvedFieldName = $this->resolveFieldName($media, $fieldName);
    $selectedOutputs = $options['selected_outputs'] ?? [];
    $streamingOptions = $options['streaming_options'] ?? [];
    $operation = $options['operation'] ?? 'convert';

    return $this->queueManager->queueForConversion(
      $media,
      $operation,
      $profileId,
      $selectedOutputs,
      $streamingOptions,
      $resolvedFieldName,
    );
  }

  /**
   * Gets the conversion status for a media item.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param string $fieldName
   *   Optional source field name override.
   *
   * @return array{status: string, progress: int, message: string|null, started: int|null, updated: int|null, elapsed_seconds: float|null}
   *   Status information.
   */
  #[\Override]
  #[\NoDiscard]
  public function getStatus(MediaInterface $media, string $fieldName = ''): array {
    $resolvedFieldName = $this->resolveFieldName($media, $fieldName);
    $progress = $this->progressTracker->getProgress((int) $media->id(), $resolvedFieldName);

    if ($progress === NULL) {
      // If outputs exist, the conversion completed but progress was cleaned up.
      $outputs = $this->getOutputs($media, $resolvedFieldName);
      if (!empty($outputs)) {
        return [
          'status' => 'completed',
          'progress' => 100,
          'message' => NULL,
          'started' => NULL,
          'updated' => NULL,
          'elapsed_seconds' => NULL,
        ];
      }

      return [
        'status' => 'none',
        'progress' => 0,
        'message' => NULL,
        'started' => NULL,
        'updated' => NULL,
        'elapsed_seconds' => NULL,
      ];
    }

    // Translate internal 'converting' status to UI-friendly 'processing'.
    $statusValue = $progress->status === ConversionStatus::Converting
      ? 'processing'
      : $progress->status->value;

    return [
      'status' => $statusValue,
      'progress' => $progress->percent,
      'message' => $progress->currentStep,
      'started' => $progress->startedAt,
      'updated' => $progress->updatedAt,
      'error' => $progress->error,
      'elapsed_seconds' => $progress->elapsedSeconds,
    ];
  }

  /**
   * Checks if a media item is currently being processed.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   *
   * @return bool
   *   TRUE if processing.
   */
  #[\NoDiscard]
  public function isProcessing(MediaInterface $media): bool {
    $progress = $this->progressTracker->getProgress((int) $media->id());
    // Check against internal 'converting' status directly.
    return $progress !== NULL && $progress->status === ConversionStatus::Converting;
  }

  /**
   * Checks if a media item has completed transcoding.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   *
   * @return bool
   *   TRUE if completed.
   */
  #[\NoDiscard]
  public function isCompleted(MediaInterface $media): bool {
    $status = $this->getStatus($media);
    return $status['status'] === 'completed';
  }

  /**
   * Gets the outputs for a media item.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param string $fieldName
   *   Optional source field name override.
   *
   * @phpstan-return array<string, array{type: string, label: string, values: array<int, array{fid: int|string|null, filename: string|null, size: int|null, url: string}>}>
   */
  #[\Override]
  #[\NoDiscard]
  public function getOutputs(MediaInterface $media, string $fieldName = ''): array {
    $mediaId = (int) $media->id();
    $resolvedFieldName = $this->resolveFieldName($media, $fieldName);
    $allOutputs = $this->outputStorage->getAllOutputs($mediaId, $resolvedFieldName);
    $outputs = [];

    foreach ($allOutputs as $type => $files) {
      $values = [];
      foreach ($files as $fileEntity) {
        $size = $fileEntity->getSize();

        // For streaming outputs, calculate total directory size
        // (manifest + all segments) instead of just the manifest.
        $uri = $fileEntity->getFileUri();
        if (in_array($type, ['hls', 'dash'], TRUE) && $uri !== NULL) {
          $size = $this->calculateStreamingDirectorySize($uri);
        }

        $values[] = [
          'fid' => $fileEntity->id(),
          'filename' => $fileEntity->getFilename(),
          'size' => $size,
          'url' => $fileEntity->createFileUrl(FALSE),
        ];
      }

      if ($values !== []) {
        $outputs[$type] = [
          'type' => $type,
          'label' => ConversionProfile::labelFromKey($type),
          'values' => $values,
        ];
      }
    }

    return $outputs;
  }

  /**
   * Clears all transcoded outputs for a media item.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param bool $deleteFiles
   *   Whether to delete the actual files.
   * @param string $fieldName
   *   Optional source field name override.
   *
   * @return bool
   *   TRUE if cleared successfully.
   */
  #[\Override]
  public function clearOutputs(MediaInterface $media, bool $deleteFiles = TRUE, string $fieldName = ''): bool {
    $resolvedFieldName = $this->resolveFieldName($media, $fieldName);

    if ($this->artifactManager !== NULL) {
      $deletedArtifacts = $this->artifactManager->deleteArtifacts(
        $media,
        deleteLocalFiles: $deleteFiles,
        deleteRemoteFiles: $deleteFiles,
        deleteProgress: $resolvedFieldName === '',
        deleteQueueState: FALSE,
        fieldName: $resolvedFieldName,
      );
      unset($deletedArtifacts);
      return TRUE;
    }

    $mediaId = (int) $media->id();
    $this->outputStorage->deleteAllOutputs($mediaId, $resolvedFieldName);
    if ($resolvedFieldName === '') {
      $this->progressTracker->deleteProgress($mediaId, $fieldName);
    }
    return TRUE;
  }

  /**
   * Re-queues a media item for transcoding (refresh).
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param bool $clearFirst
   *   Whether to clear existing outputs first.
   * @param string $fieldName
   *   Optional source field name override.
   *
   * @return bool
   *   TRUE if re-queued successfully.
   */
  #[\Override]
  public function refresh(MediaInterface $media, bool $clearFirst = TRUE, string $fieldName = ''): bool {
    if ($clearFirst) {
      $this->clearOutputs($media, TRUE, $fieldName);
    }

    return $this->queueForTranscode($media, fieldName: $fieldName);
  }

  /**
   * Gets video information for a media item.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param string $fieldName
   *   Optional source field name override.
   *
   * @return array{duration: float, width: int, height: int, codec: string, format: string, bitrate: int|null, framerate: float|null}|null
   *   Video info or NULL if not available.
   */
  #[\Override]
  #[\NoDiscard]
  public function getVideoInfo(MediaInterface $media, string $fieldName = ''): ?array {
    $sourceFile = $this->getSourceFile($media, $fieldName);

    if ($sourceFile === NULL) {
      return NULL;
    }

    $videoInfo = $this->ffmpegExecutor->getVideoInfo($sourceFile);

    if ($videoInfo === NULL) {
      return NULL;
    }

    return [
      'duration' => $videoInfo->duration,
      'width' => $videoInfo->width,
      'height' => $videoInfo->height,
      'codec' => $videoInfo->codec,
      'format' => $videoInfo->format,
      'bitrate' => $videoInfo->bitrate ?? NULL,
      'framerate' => $videoInfo->frameRate ?? NULL,
    ];
  }

  /**
   * Gets the source file path for a media item.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param string $fieldName
   *   Optional source field name override.
   *
   * @return string|null
   *   File path or NULL.
   */
  #[\Override]
  #[\NoDiscard]
  public function getSourceFile(MediaInterface $media, string $fieldName = ''): ?string {
    $resolvedFieldName = $this->resolveFieldName($media, $fieldName);
    if ($resolvedFieldName === '' || !$media->hasField($resolvedFieldName)) {
      return NULL;
    }

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

    /** @var \Drupal\file\FileInterface|null $file */
    $file = $field->entity;
    if (!$file instanceof FileInterface) {
      return NULL;
    }

    $uri = $file->getFileUri();
    if ($uri === NULL) {
      return NULL;
    }
    return $this->fileSystem->realpath($uri) ?: NULL;
  }

  /**
   * Checks if a media item is currently queued for transcoding.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param string $fieldName
   *   Optional field name override for field-specific queue checks.
   *
   * @return bool
   *   TRUE if queued.
   */
  #[\NoDiscard]
  public function isQueued(MediaInterface $media, string $fieldName = ''): bool {
    $resolvedFieldName = $this->resolveFieldName($media, $fieldName);
    return $this->queueManager->isQueued((int) $media->id(), $resolvedFieldName);
  }

  /**
   * Cancels a queued transcoding for a media item.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param string $fieldName
   *   Optional field name override for field-specific queue cancellation.
   */
  public function cancelQueuedTranscoding(MediaInterface $media, string $fieldName = ''): void {
    $mediaId = (int) $media->id();
    $resolvedFieldName = $this->resolveFieldName($media, $fieldName);
    $this->queueManager->removeFromQueue($mediaId, $resolvedFieldName);
    $this->progressTracker->deleteProgress($mediaId, $resolvedFieldName);
  }

  /**
   * Resolves the effective field name for UI operations.
   */
  #[\Override]
  #[\NoDiscard]
  public function resolveFieldName(MediaInterface $media, string $fieldName = ''): string {
    if ($fieldName !== '') {
      // Validate that the requested field has an FFmpeg formatter configured.
      // This prevents unconfigured fields from being processed via URL
      // manipulation (e.g. ?field_name=field_body on a text field).
      $context = $this->formatterSettingsResolver->getContextForField($media, $fieldName);
      if ($context !== NULL) {
        return $fieldName;
      }

      // Reject unconfigured field names — fall through to preferred context.
      $this->logger->warning('Rejected unconfigured field_name @field for media @id; falling back to preferred context.', [
        '@field' => $fieldName,
        '@id' => (int) $media->id(),
      ]);
    }
    else {
      // Log when falling back to preferred context — aids debugging for
      // multi-field media entities where implicit resolution may be wrong.
      $this->logger->debug('No explicit field_name for media @id; using preferred context fallback.', [
        '@id' => (int) $media->id(),
      ]);
    }

    $preferredContext = $this->formatterSettingsResolver->getPreferredContextForEntity($media);
    if ($preferredContext !== NULL) {
      return $preferredContext->fieldName;
    }

    $source = $media->getSource();
    if ($source === NULL) {
      return '';
    }
    $sourceField = $source->getConfiguration()['source_field'] ?? NULL;

    return is_string($sourceField) ? $sourceField : '';
  }

  /**
   * Calculates total size of a streaming directory (manifest + segments).
   *
   * @param string $manifestUri
   *   The manifest file stream URI.
   *
   * @return int
   *   Total size in bytes, or 0 if directory cannot be read.
   */
  protected function calculateStreamingDirectorySize(string $manifestUri): int {
    $realPath = $this->fileSystem->realpath($manifestUri);
    if ($realPath === FALSE) {
      return 0;
    }

    $directory = dirname($realPath);
    if (!is_dir($directory)) {
      return 0;
    }

    $totalSize = 0;
    $iterator = new \RecursiveIteratorIterator(
      new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
    );

    /** @var \SplFileInfo $file */
    foreach ($iterator as $file) {
      if ($file->isFile()) {
        $totalSize += $file->getSize();
      }
    }

    return $totalSize;
  }

}
