<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\ffmpeg_media\Enum\ConversionStatus;
use Drupal\ffmpeg_media\Enum\HardwareType;
use Drupal\ffmpeg_media\Enum\OutputType;
use Drupal\ffmpeg_media\Exception\OutputExecutionException;
use Drupal\ffmpeg_media\ValueObject\ConversionOptions;
use Drupal\ffmpeg_media\ValueObject\OutputExecutionRequest;
use Drupal\ffmpeg_media\ValueObject\ConversionProfile;
use Drupal\ffmpeg_media\ValueObject\ConversionResult;
use Drupal\ffmpeg_media\ValueObject\SingleOutputConversionRequest;
use Drupal\media\MediaInterface;

/**
 * Executes single-file and adaptive output generation for conversions.
 *
 * @internal
 */
final class OutputExecutionRunner implements OutputExecutionRunnerInterface {

  /**
   * Constructs the output execution runner.
   */
  public function __construct(
    private readonly AdaptiveStreamingConverterInterface $adaptiveConverter,
    private readonly PluginFactoryInterface $pluginFactory,
    private readonly ProfileManagerInterface $profileManager,
    private readonly ConversionFileManagerInterface $conversionFileManager,
    private readonly ConversionExecutorInterface $conversionExecutor,
    private readonly OutputStorageServiceInterface $outputStorage,
    private readonly ProgressTrackerInterface $progressTracker,
    private readonly FileSystemInterface $fileSystem,
    private readonly LoggerChannelInterface $logger,
    private readonly ?HardwareAccelerationManagerInterface $hwAccelManager = NULL,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function run(OutputExecutionRequest $request): array {
    $outputs = [];
    $failedOutputs = [];
    $lastHardwareType = NULL;
    $singleIndex = 0;

    foreach ($request->enabledOutputs as $outputIdx => $outputConfig) {
      $container = $outputConfig->container;
      $outputKey = $request->executionPlan->getOutputKey($outputIdx, $container);

      if (in_array($container, [OutputType::Hls->value, OutputType::Dash->value], TRUE)) {
        $this->logger->info('Processing adaptive streaming output: @container (correlation: @correlation_id)', [
          '@container' => $container,
          '@correlation_id' => $request->correlationId,
        ]);

        try {
          $adaptiveResult = $this->adaptiveConverter->convert($request->media, $container, $request->resolvedProfileId, $request->context->fieldName);
          if ($adaptiveResult->isSuccess()) {
            $outputs[$container] = $adaptiveResult->outputs[$container] ?? '';
          }
          else {
            $failedOutputs[$outputKey] = $adaptiveResult->getError() ?? 'Adaptive conversion produced no output.';
          }
        }
        catch (\Throwable $e) {
          $this->logger->error('Adaptive streaming conversion failed for @container: @error (correlation: @correlation_id)', [
            '@container' => $container,
            '@error' => $e->getMessage(),
            '@correlation_id' => $request->correlationId,
          ]);
          $failedOutputs[$outputKey] = $e->getMessage();
        }

        continue;
      }

      $singleIndex++;
      $singleResult = $this->convertSingleOutput(new SingleOutputConversionRequest(
        sourcePath: $request->sourcePath,
        context: $request->context,
        outputConfig: $outputConfig,
        videoInfo: $request->videoInfo,
        mediaId: $request->mediaId,
        currentOutput: $singleIndex,
        totalOutputs: $request->executionPlan->singleOutputCount,
        synchronous: $request->synchronous,
        outputDirectory: $request->executionPlan->outputDirectory,
        hardwareRequired: $request->executionPlan->hardwareRequired,
        outputKey: $outputKey,
      ));

      if ($singleResult['hardwareType'] instanceof HardwareType) {
        $lastHardwareType = $singleResult['hardwareType'];
      }

      if ($singleResult['outputUri'] !== NULL) {
        $outputs[$outputKey] = $singleResult['outputUri'];
      }
      else {
        $failedOutputs[$outputKey] = "Output format {$outputKey} produced no file.";
      }
    }

    return [
      'outputs' => $outputs,
      'failedOutputs' => $failedOutputs,
      'hardwareType' => $lastHardwareType,
    ];
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function convertAdaptive(MediaInterface $media, string $format, ?string $profileId = NULL, string $fieldName = ''): ConversionResult {
    return $this->adaptiveConverter->convert($media, $format, $profileId, $fieldName);
  }

  /**
   * Converts a single non-adaptive output.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\SingleOutputConversionRequest $req
   *   The single output conversion request.
   *
   * @return array{outputUri: string|null, hardwareType: \Drupal\ffmpeg_media\Enum\HardwareType|null}
   *   Output URI and the hardware type used for the output.
   */
  #[\NoDiscard]
  private function convertSingleOutput(SingleOutputConversionRequest $req): array {
    $outputConfig = $req->outputConfig;
    $mediaId = $req->mediaId;
    $container = $outputConfig->container;
    $outputKey = $req->outputKey ?? $container;
    $isQualified = ConversionProfile::isQualifiedKey($outputKey);
    $codecSuffix = $isQualified ? $outputConfig->codec : '';
    $currentHardwareType = NULL;

    try {
      $outputPlugin = $this->pluginFactory->createOutputInstance($container);
      $codecId = $outputConfig->codec;
      $codecPlugin = $this->pluginFactory->createCodecInstance($codecId);

      if (!$codecPlugin->isAvailable()) {
        $this->logger->warning('Codec @codec not available, skipping @container output.', [
          '@codec' => $codecId,
          '@container' => $container,
        ]);
        return [
          'outputUri' => NULL,
          'hardwareType' => NULL,
        ];
      }

      // Pre-check: if hardware acceleration is required but no hardware
      // encoder is available, skip immediately instead of wasting CPU/IO.
      if ($req->hardwareRequired) {
        $hwCheck = $this->hwAccelManager?->selectEncoder($codecPlugin, $codecId);
        if (($hwCheck['hwType'] ?? NULL) === NULL) {
          $this->logger->warning('Skipping @container output: profile requires GPU hardware acceleration but no hardware encoder is available for codec @codec.', [
            '@container' => $container,
            '@codec' => $codecPlugin->getPluginId(),
          ]);
          return [
            'outputUri' => NULL,
            'hardwareType' => NULL,
          ];
        }
      }

      $conversionOptions = $this->profileManager->applyGlobalAudioDefaults(
        ConversionOptions::fromArray($outputConfig->options),
      )->withMediaId($mediaId);

      if ($req->outputDirectory !== NULL && $req->outputDirectory !== '') {
        $outputCandidate = $this->conversionFileManager->generateOutputPathInDirectory(
          $req->sourcePath,
          $req->outputDirectory,
          $outputPlugin->getExtension(),
          $mediaId,
          $codecSuffix,
          $req->context->fieldName,
        );
      }
      else {
        $outputCandidate = $outputPlugin->generateOutputPath($req->sourcePath);
        if ($codecSuffix !== '') {
          $extension = '.' . $outputPlugin->getExtension();
          $outputCandidate = str_replace('_converted' . $extension, '_converted_' . $codecSuffix . $extension, $outputCandidate);
        }
      }

      $outputUri = $this->conversionFileManager->getStorageUri($outputCandidate);
      $outputDir = dirname($outputUri);
      $this->fileSystem->prepareDirectory($outputDir, FileSystemInterface::CREATE_DIRECTORY);
      $outputPath = $this->conversionFileManager->getLocalPath($outputUri);

      $baseProgress = (int) (($req->currentOutput - 1) / $req->totalOutputs * 100);
      $this->progressTracker->updateProgress(
        $mediaId,
        ConversionStatus::Converting,
        $baseProgress,
        "Converting to {$container}",
        $container,
        NULL,
        $req->context->fieldName,
      );

      $totalDuration = $req->videoInfo->duration;

      if ($conversionOptions->twoPass) {
        if ($this->hwAccelManager === NULL) {
          $this->logger->debug('Hardware acceleration not available for two-pass GPU routing: HardwareAccelerationManagerInterface not injected.');
        }
        $gpuResult = $this->hwAccelManager?->selectEncoder($codecPlugin, $codecId);
        $gpuHwType = $gpuResult['hwType'] ?? NULL;
        $gpuEncoder = $gpuResult['encoder'] ?? NULL;

        if ($gpuHwType !== NULL && $gpuEncoder !== NULL) {
          $gpuOptions = $conversionOptions->withTwoPass(FALSE);
          if ($gpuHwType->supportsNativeMultipass()) {
            $gpuOptions = $gpuOptions->withExtraArgs(
              array_merge($conversionOptions->extraArgs, ['-multipass', 'fullres']),
            );
          }
          $currentHardwareType = $this->conversionExecutor->executeWithFallback(
            $req->sourcePath,
            $outputPath,
            $codecPlugin,
            $outputPlugin,
            $gpuOptions,
            $req->videoInfo,
            $mediaId,
            $baseProgress,
            $req->totalOutputs,
            $container,
            $totalDuration,
            $req->synchronous,
            $req->context->fieldName,
          );
        }
        else {
          $currentHardwareType = $this->conversionExecutor->executeTwoPassConversion(
            $req->sourcePath,
            $outputPath,
            $codecPlugin,
            $outputPlugin,
            $conversionOptions,
            $mediaId,
            $container,
            $baseProgress,
            $req->totalOutputs,
            $req->context->fieldName,
          );
        }
      }
      else {
        $currentHardwareType = $this->conversionExecutor->executeWithFallback(
          $req->sourcePath,
          $outputPath,
          $codecPlugin,
          $outputPlugin,
          $conversionOptions,
          $req->videoInfo,
          $mediaId,
          $baseProgress,
          $req->totalOutputs,
          $container,
          $totalDuration,
          $req->synchronous,
          $req->context->fieldName,
        );
      }

      if (file_exists($outputPath) && filesize($outputPath) > 0) {
        $file = $this->conversionFileManager->createFileEntity($outputUri, $outputPlugin->getMimeType());
        $this->outputStorage->setOutput($mediaId, $req->context->fieldName, $outputKey, $file);
        return [
          'outputUri' => $outputUri,
          'hardwareType' => $currentHardwareType,
        ];
      }

      return [
        'outputUri' => NULL,
        'hardwareType' => $currentHardwareType,
      ];
    }
    catch (OutputExecutionException $e) {
      throw $e;
    }
    catch (\Throwable $e) {
      throw new OutputExecutionException($e->getMessage(), $currentHardwareType, $e);
    }
  }

}
