<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\ffmpeg_media\Enum\ConversionStatus;
use Drupal\ffmpeg_media\Enum\HardwareType;
use Drupal\ffmpeg_media\Event\ConversionProgressEvent;
use Drupal\ffmpeg_media\Event\FFmpegMediaEvents;
use Drupal\ffmpeg_media\Exception\FFmpegException;
use Drupal\ffmpeg_media\Plugin\FFmpegCodec\FFmpegCodecInterface;
use Drupal\ffmpeg_media\Plugin\FFmpegOutput\FFmpegOutputInterface;
use Drupal\ffmpeg_media\ValueObject\ConversionOptions;
use Drupal\ffmpeg_media\ValueObject\VideoInfo;
use Drupal\media\MediaInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
 * Executes FFmpeg conversions with hardware fallback and progress tracking.
 *
 * @internal
 */
final class ConversionExecutor implements ConversionExecutorInterface {

  /**
   * Constructs a ConversionExecutor.
   *
   * @param \Drupal\ffmpeg_media\Service\FfmpegCommandBuilderInterface $commandBuilder
   *   Command builder.
   * @param \Drupal\ffmpeg_media\Service\FFmpegExecutorInterface $ffmpegExecutor
   *   FFmpeg executor.
   * @param \Drupal\ffmpeg_media\Service\HardwareDetectorInterface $hardwareDetector
   *   Hardware detector.
   * @param \Drupal\ffmpeg_media\Service\ProgressTrackerInterface $progressTracker
   *   Progress tracker.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
   *   Event dispatcher.
   * @param \Drupal\ffmpeg_media\Service\ConfigValueProviderInterface $configValueProvider
   *   Config value provider for cached config reads.
   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
   *   Logger.
   * @param \Drupal\ffmpeg_media\Service\TwoPassEncodingServiceInterface|null $twoPassService
   *   Optional two-pass encoding service.
   * @param \Drupal\ffmpeg_media\Service\MetricsServiceInterface|null $metricsService
   *   Optional metrics service.
   */
  public function __construct(
    private readonly FfmpegCommandBuilderInterface $commandBuilder,
    private readonly FFmpegExecutorInterface $ffmpegExecutor,
    private readonly HardwareDetectorInterface $hardwareDetector,
    private readonly ProgressTrackerInterface $progressTracker,
    private readonly EntityTypeManagerInterface $entityTypeManager,
    private readonly EventDispatcherInterface $eventDispatcher,
    private readonly ConfigValueProviderInterface $configValueProvider,
    private readonly LoggerChannelInterface $logger,
    private readonly ?TwoPassEncodingServiceInterface $twoPassService = NULL,
    private readonly ?MetricsServiceInterface $metricsService = NULL,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function executeWithFallback(
    string $sourcePath,
    string $outputPath,
    FFmpegCodecInterface $codecPlugin,
    FFmpegOutputInterface $outputPlugin,
    ConversionOptions $conversionOptions,
    ?VideoInfo $videoInfo,
    int $mediaId,
    int $baseProgress,
    int $totalOutputs,
    string $container,
    ?float $totalDuration,
    bool $synchronous,
    string $fieldName = '',
  ): ?HardwareType {
    $result = $this->commandBuilder->buildOutputCommand(
      $sourcePath,
      $outputPath,
      $codecPlugin,
      $outputPlugin,
      $conversionOptions,
      $videoInfo,
    );
    $command = $result['command'];
    $currentHwType = $result['hwType'];

    try {
      $this->executeCommand($command, $conversionOptions, $mediaId, $baseProgress, $totalOutputs, $container, $totalDuration, $synchronous, $fieldName);
      return $currentHwType;
    }
    catch (FFmpegException $e) {
      $fallbackEnabled = $this->configValueProvider->isHardwareFallbackEnabled();
      if (!$fallbackEnabled || $currentHwType === NULL) {
        throw $e;
      }

      $errorOutput = $e->getOutput() ?? $e->getMessage();
      if (!$this->ffmpegExecutor->isHardwareError($errorOutput, $currentHwType->value)) {
        throw $e;
      }

      $this->logger->warning('Hardware encoder @hw failed for @container, retrying with software encoder: @error', [
        '@hw' => $currentHwType->value,
        '@container' => $container,
        '@error' => $e->getMessage(),
      ]);
      $this->metricsService?->recordHardwareFallback($currentHwType->value, $e->getMessage());

      $this->hardwareDetector->recordEncodingFailure($currentHwType->value);

      // Rebuild command without hardware acceleration.
      $softwareResult = $this->commandBuilder->buildOutputCommand(
        $sourcePath,
        $outputPath,
        $codecPlugin,
        $outputPlugin,
        $conversionOptions,
        $videoInfo,
        softwareOnly: TRUE,
      );

      $this->executeCommand($softwareResult['command'], $conversionOptions, $mediaId, $baseProgress, $totalOutputs, $container, $totalDuration, $synchronous, $fieldName);

      // Return NULL since we fell back to software.
      return NULL;
    }
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function executeTwoPassConversion(
    string $sourcePath,
    string $outputPath,
    FFmpegCodecInterface $codecPlugin,
    FFmpegOutputInterface $outputPlugin,
    ConversionOptions $options,
    int $mediaId,
    string $container,
    int $baseProgress,
    int $totalOutputs,
    string $fieldName = '',
  ): ?HardwareType {
    if ($this->twoPassService === NULL) {
      $this->logger->debug('Two-pass encoding not available: TwoPassEncodingServiceInterface not injected. Enable the ffmpeg_media_advanced submodule for two-pass support.');
      throw new FFmpegException('Two-pass encoding requested but service not available.');
    }

    // Check if codec supports two-pass.
    if (!$this->twoPassService->isTwoPassSupported($codecPlugin)) {
      $this->logger->warning('Two-pass encoding not supported for codec @codec, falling back to single-pass.', [
        '@codec' => $codecPlugin->getEncoder(),
      ]);

      // Fall back to single-pass while preserving output/container flags.
      $softwareResult = $this->commandBuilder->buildOutputCommand(
        $sourcePath,
        $outputPath,
        $codecPlugin,
        $outputPlugin,
        $options,
        NULL,
        softwareOnly: TRUE,
      );
      (void) $this->ffmpegExecutor->execute($softwareResult['command'], ['timeout' => $options->timeout]);
      return NULL;
    }

    // Update progress for pass 1.
    $this->progressTracker->updateProgress(
      $mediaId,
      ConversionStatus::Converting,
      $baseProgress,
      "Two-pass encoding (pass 1) to {$container}",
      $container,
      NULL,
      $fieldName,
    );

    // Execute two-pass encoding.
    $success = $this->twoPassService->encode(
      $sourcePath,
      $outputPath,
      $codecPlugin,
      $options,
    );

    if (!$success) {
      throw new FFmpegException("Two-pass encoding failed for {$container}.");
    }

    // Update progress after completion.
    $outputProgress = (int) round(100 / $totalOutputs);
    $totalProgress = $baseProgress + $outputProgress;
    $this->progressTracker->updateProgress(
      $mediaId,
      ConversionStatus::Converting,
      min(99, $totalProgress),
      "Completed {$container}",
      $container,
      NULL,
      $fieldName,
    );

    return NULL;
  }

  /**
   * Executes an FFmpeg command with optional progress tracking.
   *
   * @param array<string> $command
   *   FFmpeg command as array of arguments.
   * @param \Drupal\ffmpeg_media\ValueObject\ConversionOptions $conversionOptions
   *   Conversion options.
   * @param int $mediaId
   *   Media entity ID.
   * @param int $baseProgress
   *   Base progress percentage for this output.
   * @param int $totalOutputs
   *   Total number of outputs being converted.
   * @param string $container
   *   Output container format name.
   * @param float|null $totalDuration
   *   Total video duration in seconds.
   * @param bool $synchronous
   *   Whether conversion is synchronous.
   * @param string $fieldName
   *   Scoped media field name for progress updates.
   */
  private function executeCommand(
    array $command,
    ConversionOptions $conversionOptions,
    int $mediaId,
    int $baseProgress,
    int $totalOutputs,
    string $container,
    ?float $totalDuration,
    bool $synchronous,
    string $fieldName = '',
  ): void {
    if ($synchronous) {
      (void) $this->ffmpegExecutor->execute($command, [
        'timeout' => $conversionOptions->timeout,
      ]);
      return;
    }

    // Load media entity once outside the closure.
    $mediaEntity = $this->entityTypeManager->getStorage('media')->load($mediaId);

    $progressCallback = function (int $percent, ?float $elapsedSeconds = NULL) use ($mediaId, $mediaEntity, $baseProgress, $totalOutputs, $container, $fieldName): void {
      if ($percent === -1) {
        // Duration unknown — update with elapsed encoding time only.
        $this->progressTracker->updateProgress(
          $mediaId,
          ConversionStatus::Converting,
          0,
          "Converting to {$container}",
          $container,
          $elapsedSeconds,
          $fieldName,
        );
      }
      else {
        $outputProgress = (int) round($percent / $totalOutputs);
        $totalProgress = $baseProgress + $outputProgress;
        $this->progressTracker->updateProgress(
          $mediaId,
          ConversionStatus::Converting,
          min(99, $totalProgress),
          "Converting to {$container}",
          $container,
          NULL,
          $fieldName,
        );
      }

      $progress = $this->progressTracker->getProgress($mediaId, $fieldName);
      if ($progress !== NULL && $mediaEntity instanceof MediaInterface) {
        $this->eventDispatcher->dispatch(
          new ConversionProgressEvent($mediaEntity, $progress),
          FFmpegMediaEvents::CONVERSION_PROGRESS,
        );
      }
    };

    (void) $this->ffmpegExecutor->executeWithProgress(
      $command,
      $mediaId,
      $totalDuration,
      [
        'timeout' => $conversionOptions->timeout,
        'progress_callback' => $progressCallback,
      ],
    );
  }

}
