<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\ffmpeg_media\Enum\OutputType;
use Drupal\ffmpeg_media\FFmpegProcessorPluginManager;
use Drupal\ffmpeg_media\Plugin\FFmpegProcessor\FFmpegProcessorInterface;
use Drupal\ffmpeg_media\ValueObject\ConversionContext;
use Drupal\ffmpeg_media\ValueObject\VideoInfo;
use Drupal\media\MediaInterface;

/**
 * Runs the FFmpeg processor pipeline against source videos.
 *
 * Orchestrates enabled processors (thumbnail, GIF, HDR, watermark, etc.)
 * and stores outputs to mapped fields via ConversionFileManager.
 *
 * @internal
 */
final class ProcessorPipelineRunner implements ProcessorPipelineRunnerInterface {

  /**
   * Constructs a ProcessorPipelineRunner.
   *
   * @param \Drupal\ffmpeg_media\FFmpegProcessorPluginManager $processorManager
   *   Processor plugin manager.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   Config factory service.
   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
   *   File system service.
   * @param \Drupal\ffmpeg_media\Service\OutputStorageServiceInterface $outputStorage
   *   Output storage service.
   * @param \Drupal\ffmpeg_media\Service\ConversionFileManagerInterface $conversionFileManager
   *   Conversion file manager service.
   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
   *   Logger channel.
   * @param \Drupal\ffmpeg_media\Service\ProfileManagerInterface $profileManager
   *   Profile manager service.
   */
  public function __construct(
    protected readonly FFmpegProcessorPluginManager $processorManager,
    protected readonly ConfigFactoryInterface $configFactory,
    protected readonly FileSystemInterface $fileSystem,
    protected readonly OutputStorageServiceInterface $outputStorage,
    protected readonly ConversionFileManagerInterface $conversionFileManager,
    protected readonly LoggerChannelInterface $logger,
    protected readonly ProfileManagerInterface $profileManager,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function runProcessorPipeline(
    MediaInterface $media,
    string $sourcePath,
    ConversionContext $context,
    ?VideoInfo $videoInfo,
    ?string $profileId = NULL,
  ): array {
    $config = $this->configFactory->get('ffmpeg_media.settings');
    $enabledProcessors = $profileId !== NULL
      ? $this->profileManager->getEnabledProcessors($profileId)
      : ($config->get('enabled_processors') ?? ['thumbnail', 'video_info']);
    $outputs = [];

    foreach ($enabledProcessors as $processorId) {
      try {
        // Check if processor exists and is available.
        if (!$this->processorManager->hasDefinition($processorId)) {
          continue;
        }

        $processor = $this->processorManager->createInstance($processorId);
        if (!$processor instanceof FFmpegProcessorInterface) {
          throw new \UnexpectedValueException(sprintf(
            'Expected %s, got %s for processor plugin %s.',
            FFmpegProcessorInterface::class,
            get_debug_type($processor),
            $processorId,
          ));
        }

        if (!$processor->isAvailable()) {
          $this->logger->debug('Processor @processor not available, skipping.', [
            '@processor' => $processorId,
          ]);
          continue;
        }

        // Determine processor options based on type.
        $options = $this->getProcessorOptions($processorId, $videoInfo, $config, $processor);

        // Skip manifest processor if manifest generation is disabled.
        if ($processorId === 'manifest' && !$config->get('manifest_enabled')) {
          continue;
        }

        // Skip HDR processor if video content is not HDR.
        if ($processorId === 'hdr' && !$this->isHdrContent($videoInfo)) {
          continue;
        }

        // Generate output path for this processor.
        $outputPath = $processor->generateOutputPath($sourcePath, $options);
        $outputUri = $this->conversionFileManager->getStorageUri($outputPath);

        // Ensure output directory exists.
        $outputDir = dirname($outputUri);
        $this->fileSystem->prepareDirectory($outputDir, FileSystemInterface::CREATE_DIRECTORY);
        $localOutputPath = $this->conversionFileManager->getLocalPath($outputUri);

        // Process.
        $success = $processor->process($sourcePath, $localOutputPath, $options);

        if ($success && file_exists($localOutputPath)) {
          $outputs[$processorId] = $outputUri;

          // Store output via OutputStorage if applicable.
          $this->storeProcessorOutput($media, $context, $processorId, $outputUri);

          $this->logger->info('Processor @processor completed for media @id.', [
            '@processor' => $processorId,
            '@id' => $media->id(),
          ]);
        }
      }
      catch (\Throwable $e) {
        $this->logger->error('Processor @processor failed for media @id: @error', [
          '@processor' => $processorId,
          '@id' => $media->id(),
          '@error' => $e->getMessage(),
        ]);
        // Continue with other processors even if one fails.
      }
    }

    return $outputs;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function isHdrContent(?VideoInfo $videoInfo): bool {
    if ($videoInfo === NULL) {
      return FALSE;
    }

    // Check for HDR color transfer characteristics (most reliable indicator).
    if ($videoInfo->colorTransfer !== NULL) {
      $hdrTransfers = ['smpte2084', 'arib-std-b67', 'smpte428'];
      if (in_array($videoInfo->colorTransfer, $hdrTransfers, TRUE)) {
        return TRUE;
      }
    }

    // Check for BT.2020 color primaries (wide color gamut used by HDR).
    if ($videoInfo->colorPrimaries === 'bt2020') {
      return TRUE;
    }

    // Check for 10-bit or higher pixel formats as secondary indicator.
    if ($videoInfo->pixelFormat !== NULL) {
      $tenBitFormats = ['yuv420p10le', 'yuv422p10le', 'yuv444p10le', 'yuv420p12le', 'yuv422p12le', 'yuv444p12le'];
      if (in_array($videoInfo->pixelFormat, $tenBitFormats, TRUE)) {
        // 10-bit alone isn't definitive (some SDR uses 10-bit), but combined
        // with an HDR-capable codec it's a strong indicator.
        $hdrCodecs = ['hevc', 'av1', 'vp9'];
        $codecLower = strtolower($videoInfo->codec);
        foreach ($hdrCodecs as $hdrCodec) {
          if (str_contains($codecLower, $hdrCodec)) {
            return TRUE;
          }
        }
      }
    }

    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getMimeTypeForProcessor(string $processorId): string {
    return match ($processorId) {
      'thumbnail', 'segment_thumbnail' => 'image/jpeg',
      'gif', 'segment_gif' => 'image/gif',
      'video_info', 'manifest' => 'application/json',
      'whisper' => $this->getWhisperMimeType(),
      'multi_audio', 'audio_normalize' => 'audio/aac',
      'hdr', 'watermark', 'normalize', 'subtitles' => 'video/mp4',
      default => 'application/octet-stream',
    };
  }

  /**
   * Gets options for a specific processor.
   *
   * Delegates to the processor's own getDefaultOptions() method, which
   * reads its own configuration via dependency injection. This avoids
   * the pipeline runner having direct dependencies on submodule configs.
   *
   * @param string $processorId
   *   Processor plugin ID.
   * @param \Drupal\ffmpeg_media\ValueObject\VideoInfo|null $videoInfo
   *   Video information.
   * @param \Drupal\Core\Config\ImmutableConfig $config
   *   Base module configuration object.
   * @param \Drupal\ffmpeg_media\Plugin\FFmpegProcessor\FFmpegProcessorInterface $processor
   *   The processor plugin instance.
   *
   * @return array<string, bool|int|float|string>
   *   Processor options.
   */
  protected function getProcessorOptions(string $processorId, ?VideoInfo $videoInfo, $config, FFmpegProcessorInterface $processor): array {
    // Each processor provides its own defaults, reading its own config
    // via dependency injection. No submodule config coupling here.
    $options = $processor->getDefaultOptions();

    // Apply video-info-aware overrides that require pipeline context.
    if ($processorId === 'thumbnail' && $videoInfo !== NULL && $videoInfo->duration > 5) {
      $options['timestamp'] = min($videoInfo->duration * 0.1, 5.0);
    }

    return $options;
  }

  /**
   * Stores processor output via OutputStorage.
   *
   * @param \Drupal\media\MediaInterface $media
   *   Media entity.
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionContext $context
   *   Conversion context.
   * @param string $processorId
   *   Processor ID.
   * @param string $outputPath
   *   Output file URI or path.
   */
  protected function storeProcessorOutput(
    MediaInterface $media,
    ConversionContext $context,
    string $processorId,
    string $outputPath,
  ): void {
    // Map processor IDs to output type enum cases.
    $outputTypeMap = [
      'thumbnail' => OutputType::Thumbnail,
      'segment_thumbnail' => OutputType::SegmentThumbnails,
      'gif' => OutputType::Gif,
      'segment_gif' => OutputType::SegmentGifs,
      'video_info' => OutputType::Metadata,
      'manifest' => OutputType::Manifest,
      'whisper' => OutputType::Transcript,
      'multi_audio' => OutputType::MultiAudio,
      'audio_normalize' => OutputType::AudioNormalized,
      'watermark' => OutputType::Watermarked,
      'normalize' => OutputType::Normalized,
      'subtitles' => OutputType::Subtitled,
    ];

    $outputType = $outputTypeMap[$processorId] ?? NULL;

    // HDR processor outputs to different types based on mode.
    if ($processorId === 'hdr') {
      $outputType = str_ends_with($outputPath, '_hdr.mp4') ? OutputType::HdrOutput : OutputType::SdrOutput;
    }

    if ($outputType !== NULL) {
      $mimeType = $this->getMimeTypeForProcessor($processorId);
      $file = $this->conversionFileManager->createFileEntity($outputPath, $mimeType);
      $this->outputStorage->setOutput((int) $media->id(), $context->fieldName, $outputType->value, $file);
    }
  }

  /**
   * Resolves the configured MIME type for Whisper transcript output.
   *
   * @return string
   *   Transcript MIME type.
   */
  private function getWhisperMimeType(): string {
    $format = strtolower((string) ($this->configFactory
      ->get('ffmpeg_media_whisper.settings')
      ->get('output_format') ?? 'srt'));

    return match ($format) {
      'vtt' => 'text/vtt',
      'srt' => 'application/x-subrip',
      default => 'text/plain',
    };
  }

}
