<?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\ConversionStatus;
use Drupal\ffmpeg_media\Enum\OutputType;
use Drupal\ffmpeg_media\Exception\FFmpegException;
use Drupal\ffmpeg_media\Exception\InvalidConfigurationException;
use Drupal\ffmpeg_media\Plugin\FFmpegOutput\AdaptiveOutputInterface;
use Drupal\ffmpeg_media\Plugin\FFmpegOutput\SubtitleAwareOutputInterface;
use Drupal\ffmpeg_media\ValueObject\ConversionResult;
use Drupal\ffmpeg_media\ValueObject\StreamingConversionDependencies;
use Drupal\ffmpeg_media\ValueObject\VideoInfo;
use Drupal\media\MediaInterface;

/**
 * Handles HLS/DASH adaptive streaming conversion workflow.
 *
 * @internal
 */
final class AdaptiveStreamingConverter implements AdaptiveStreamingConverterInterface {

  /**
   * Constructs an AdaptiveStreamingConverter.
   *
   * @param \Drupal\ffmpeg_media\ValueObject\StreamingConversionDependencies $deps
   *   Aggregated streaming conversion dependencies.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   Config factory.
   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
   *   File system.
   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
   *   Logger.
   */
  public function __construct(
    private readonly StreamingConversionDependencies $deps,
    private readonly ConfigFactoryInterface $configFactory,
    private readonly FileSystemInterface $fileSystem,
    private readonly LoggerChannelInterface $logger,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function convert(
    MediaInterface $media,
    string $format,
    ?string $profileId = NULL,
    string $fieldName = '',
  ): ConversionResult {
    $mediaId = (int) $media->id();
    $startTime = microtime(TRUE);
    $subtitleTempFiles = [];
    $commandResult = [];

    // Validate format.
    if (!in_array($format, [OutputType::Hls->value, OutputType::Dash->value], TRUE)) {
      throw InvalidConfigurationException::fromMessage("Invalid adaptive streaming format: {$format}. Must be 'hls' or 'dash'.", 'adaptive_format');
    }

    // Get conversion context — use explicit field when provided to prevent
    // cross-field collisions on multi-field media entities.
    if ($fieldName !== '') {
      $context = $this->deps->formatterSettingsResolver->getContextForField($media, $fieldName);
    }
    else {
      $context = $this->deps->formatterSettingsResolver->getPreferredContextForEntity($media);
    }
    if ($context === NULL) {
      throw InvalidConfigurationException::missingFormatterConfiguration(
        $media->getEntityTypeId(),
        $media->bundle(),
      );
    }

    // Get source path.
    $sourcePath = $this->deps->sourceFileResolver->getSourcePath($media, $context->fieldName);
    if ($sourcePath === NULL || !file_exists($sourcePath)) {
      return ConversionResult::failed($mediaId, 'Source file not found.');
    }

    // Update progress.
    $this->deps->progressTracker->updateProgress(
      $mediaId,
      ConversionStatus::Converting,
      0,
      "Starting adaptive streaming conversion ({$format})",
      NULL,
      NULL,
      $context->fieldName,
    );

    try {
      // Get output plugin for the format.
      $outputPlugin = $this->deps->pluginFactory->createOutputInstance($format);

      // Get video info for adaptive bitrate calculations.
      $videoInfo = $this->deps->ffprobeCache->getOrCompute(
        $sourcePath,
        fn(string $uri): ?VideoInfo => $this->deps->ffmpegExecutor->getVideoInfo($uri),
      );

      // Get profile options.
      $profileId = $profileId ?? $context->conversionProfile ?? $this->deps->profileManager->getDefaultProfileId();
      $conversionOptions = $this->deps->profileManager->getOptionsForFormat($profileId, $format)
        ->withMediaId($mediaId)
        ->withFieldName($context->fieldName);

      // Prepare subtitle tracks (optional).
      $subtitleContext = $this->deps->subtitleHandler->prepareSubtitleTracks($media, $context);
      $subtitleTracks = $subtitleContext['tracks'];
      $subtitleTempFiles = $subtitleContext['tempFiles'];
      $subtitleInputArgs = [];

      // Generate output path using streaming-specific directory structure.
      $outputCandidate = $this->getStreamingOutputPath($media, $format, $context->fieldName);
      $outputUri = $this->deps->conversionFileManager->getStorageUri($outputCandidate);

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

      // Build adaptive arguments with optional subtitle support.
      if (!empty($subtitleTracks) && $outputPlugin instanceof SubtitleAwareOutputInterface) {
        $adaptiveArgs = $outputPlugin->buildAdaptiveArgumentsWithSubtitles($conversionOptions, $subtitleTracks);
        $subtitleInputArgs = $outputPlugin->buildSubtitleInputArguments($subtitleTracks);
      }
      else {
        if (!empty($subtitleTracks)) {
          $this->logger->warning('Subtitle tracks are configured but output plugin @format does not support subtitle inputs.', [
            '@format' => $format,
          ]);
        }

        // Fallback to standard arguments.
        if ($outputPlugin instanceof AdaptiveOutputInterface) {
          $adaptiveArgs = $outputPlugin->buildAdaptiveArguments($conversionOptions, $videoInfo);
        }
        else {
          $adaptiveArgs = $outputPlugin->buildArguments($conversionOptions);
        }
      }

      // Get default codec for the output format.
      $definition = $outputPlugin->getPluginDefinition();
      $defaultCodec = is_array($definition) ? ($definition['defaultCodec'] ?? 'h264') : 'h264';
      $codecPlugin = $this->deps->pluginFactory->createCodecInstance($defaultCodec);

      // Build the adaptive streaming command.
      $commandResult = $this->deps->commandBuilder->buildAdaptiveCommand(
        $sourcePath,
        $outputPath,
        $codecPlugin,
        $adaptiveArgs,
        $conversionOptions,
        $videoInfo,
        $subtitleInputArgs,
      );

      // Execute the command.
      (void) $this->deps->ffmpegExecutor->executeWithProgress(
        $commandResult['command'],
        $mediaId,
        $videoInfo?->duration,
        [
          'timeout' => $conversionOptions->timeout ?? 3600,
          'progress_callback' => function (int $percent) use ($mediaId, $format, $context): void {
            $this->deps->progressTracker->updateProgress(
              $mediaId,
              ConversionStatus::Converting,
              min(99, $percent),
              "Converting to {$format}",
              $format,
              NULL,
              $context->fieldName,
            );
          },
        ],
      );

      // Verify output was created and is non-empty.
      if (!file_exists($outputPath) || filesize($outputPath) === 0) {
        throw new FFmpegException("Output file is empty or was not created: {$outputPath}");
      }

      // Store output in output storage.
      $file = $this->deps->conversionFileManager->createFileEntity($outputUri, $outputPlugin->getMimeType());
      $this->deps->outputStorage->setOutput($mediaId, $context->fieldName, $format, $file);

      $duration = microtime(TRUE) - $startTime;
      $result = ConversionResult::success($mediaId, [$format => $outputUri], $duration);

      $this->logger->info('Adaptive streaming conversion (@format) completed for media @id in @duration seconds.', [
        '@format' => $format,
        '@id' => $mediaId,
        '@duration' => round($duration, 2),
      ]);

      return $result;
    }
    catch (\Throwable $e) {
      $this->logger->error('Adaptive streaming conversion (@format) failed for media @id: @error', [
        '@format' => $format,
        '@id' => $mediaId,
        '@error' => $e->getMessage(),
      ]);
      return ConversionResult::failed($mediaId, $e->getMessage(), microtime(TRUE) - $startTime);
    }
    finally {
      $this->deps->conversionFileManager->cleanupTempFiles($subtitleTempFiles);
    }
  }

  /**
   * Gets the streaming output path for HLS/DASH formats.
   *
   * When a field name is provided, it is included as a path segment between
   * the media ID and the format to prevent collisions when multiple fields
   * on the same media entity target the same streaming format.
   *
   * @param \Drupal\media\MediaInterface $media
   *   The media entity.
   * @param string $format
   *   The streaming format ('hls' or 'dash').
   * @param string $fieldName
   *   Source field name. Inserted as path segment when non-empty.
   *
   * @return string
   *   Path to the manifest file.
   */
  private function getStreamingOutputPath(MediaInterface $media, string $format, string $fieldName = ''): string {
    $config = $this->configFactory->get('ffmpeg_media.settings');
    $outputDir = $config->get('output_directory') ?? 'public://ffmpeg_media';
    $mediaId = $media->id();

    // Field-scoped: {outputDir}/{mediaId}/{fieldName}/{format}.
    // Legacy (empty fieldName): {outputDir}/{mediaId}/{format}.
    $path = $fieldName !== ''
      ? $outputDir . '/' . $mediaId . '/' . $fieldName . '/' . $format
      : $outputDir . '/' . $mediaId . '/' . $format;

    $this->fileSystem->prepareDirectory($path, FileSystemInterface::CREATE_DIRECTORY);

    return match ($format) {
      OutputType::Hls->value => $path . '/master.m3u8',
      OutputType::Dash->value => $path . '/manifest.mpd',
      default => $path . '/output.' . $format,
    };
  }

}
