<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Plugin\FFmpegProcessor;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\File\FileExists;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\ffmpeg_media\Attribute\FFmpegProcessor;
use Drupal\ffmpeg_media\Service\FFmpegExecutorInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Yaml\Yaml;

/**
 * Processor that generates manifest files for transcode outputs.
 *
 * Creates JSON or YAML files summarizing video conversion results,
 * including output paths, metadata, and processing information.
 */
#[FFmpegProcessor(
  id: 'manifest',
  label: new TranslatableMarkup('Manifest Generator'),
  description: new TranslatableMarkup('Generates JSON/YAML manifest summarizing transcode outputs.'),
  outputType: 'data',
  outputExtension: 'json',
  requiredFilters: [],
  weight: 100,
)]
final class ManifestProcessor extends FFmpegProcessorBase {

  /**
   * Supported manifest formats.
   */
  private const string FORMAT_JSON = 'json';
  private const string FORMAT_YAML = 'yaml';

  /**
   * Default manifest format.
   */
  private const string DEFAULT_FORMAT = self::FORMAT_JSON;

  /**
   * Constructs a ManifestProcessor object.
   *
   * @param array<string, mixed> $configuration
   *   Plugin configuration.
   * @param string $plugin_id
   *   Plugin ID.
   * @param mixed $plugin_definition
   *   Plugin definition.
   * @param \Drupal\ffmpeg_media\Service\FFmpegExecutorInterface $ffmpegExecutor
   *   FFmpeg executor service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   Config factory.
   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
   *   The file system service.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    #[Autowire(service: FFmpegExecutorInterface::class)]
    FFmpegExecutorInterface $ffmpegExecutor,
    protected readonly ConfigFactoryInterface $configFactory,
    #[Autowire(service: 'file_system')]
    private readonly FileSystemInterface $fileSystem,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $ffmpegExecutor);
  }

  /**
   * {@inheritdoc}
   *
   * @param string $inputPath
   *   Input file path.
   * @param string $outputPath
   *   Output file path.
   * @param array<string, mixed> $options
   *   Processing options.
   */
  #[\Override]
  #[\NoDiscard]
  public function process(string $inputPath, string $outputPath, array $options = []): bool {
    $mergedOptions = $this->mergeOptions($options);
    $format = $mergedOptions['format'] ?? self::DEFAULT_FORMAT;

    // Build manifest data.
    $manifest = $this->buildManifest($inputPath, $mergedOptions);

    // Write manifest to file.
    $content = $this->formatManifest($manifest, $format);
    if ($content === NULL) {
      return FALSE;
    }

    return $this->fileSystem->saveData($content, $outputPath, FileExists::Replace) !== FALSE;
  }

  /**
   * Generates a manifest from transcode result data.
   *
   * This is the primary method for generating manifests from
   * a TranscodeResult or similar structured data.
   *
   * @param array<string, mixed> $resultData
   *   Result data array containing:
   *   - input_path: Original input path.
   *   - outputs: Map of format => output path.
   *   - metadata: Video metadata.
   *   - thumbnail_path: Optional thumbnail path.
   *   - duration: Processing duration.
   *   - profile_id: Profile used.
   * @param string $outputPath
   *   Where to write the manifest.
   * @param array<string, mixed> $options
   *   Additional options:
   *   - format: 'json' or 'yaml'.
   *   - include_checksums: Whether to include file checksums.
   *   - include_sizes: Whether to include file sizes.
   *
   * @return bool
   *   TRUE if manifest was generated successfully.
   */
  #[\NoDiscard]
  public function generateFromResult(
    array $resultData,
    string $outputPath,
    array $options = [],
  ): bool {
    $format = $options['format'] ?? $this->getConfiguredFormat();
    $includeChecksums = $options['include_checksums'] ?? FALSE;
    $includeSizes = $options['include_sizes'] ?? TRUE;

    $manifest = [
      'version' => '1.0',
      'generated' => date('c'),
      'generator' => 'ffmpeg_media',
      'input' => [
        'path' => $resultData['input_path'] ?? '',
        'type' => $resultData['input_type'] ?? 'path',
      ],
      'outputs' => [],
      'metadata' => $resultData['metadata'] ?? [],
      'processing' => [
        'duration_seconds' => round((float) ($resultData['duration'] ?? 0), 3),
        'profile' => $resultData['profile_id'] ?? NULL,
      ],
    ];

    // Add thumbnail if present.
    if (!empty($resultData['thumbnail_path'])) {
      $manifest['thumbnail'] = $this->buildFileInfo(
        $resultData['thumbnail_path'],
        $includeSizes,
        $includeChecksums,
      );
    }

    // Build outputs info.
    $outputs = $resultData['outputs'] ?? [];
    foreach ($outputs as $formatKey => $path) {
      $manifest['outputs'][$formatKey] = $this->buildFileInfo(
        $path,
        $includeSizes,
        $includeChecksums,
      );
    }

    // Add streaming info if present.
    if (!empty($resultData['manifest_path'])) {
      $manifest['streaming'] = [
        'manifest' => $resultData['manifest_path'],
        'type' => $resultData['streaming_type'] ?? 'hls',
      ];
    }

    // Write manifest.
    $content = $this->formatManifest($manifest, $format);
    if ($content === NULL) {
      return FALSE;
    }

    return $this->fileSystem->saveData($content, $outputPath, FileExists::Replace) !== FALSE;
  }

  /**
   * {@inheritdoc}
   *
   * @return array<string, bool|string>
   *   Default option values.
   */
  #[\Override]
  #[\NoDiscard]
  public function getDefaultOptions(): array {
    return [
      'format' => $this->getConfiguredFormat(),
      'include_checksums' => FALSE,
      'include_sizes' => TRUE,
      'pretty_print' => TRUE,
    ];
  }

  /**
   * {@inheritdoc}
   *
   * @param string $inputPath
   *   Input file path.
   * @param array<string, mixed> $options
   *   Processing options.
   */
  #[\Override]
  #[\NoDiscard]
  public function generateOutputPath(string $inputPath, array $options = []): string {
    $pathInfo = pathinfo($inputPath);
    $baseName = $pathInfo['filename'];
    $directory = $pathInfo['dirname'] ?? '';
    $format = $options['format'] ?? $this->getConfiguredFormat();
    $extension = $format === self::FORMAT_YAML ? 'yml' : 'json';

    return $directory . DIRECTORY_SEPARATOR . $baseName . '_manifest.' . $extension;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function isAvailable(): bool {
    // Manifest processor doesn't require FFmpeg features.
    return TRUE;
  }

  /**
   * Gets the configured manifest format.
   *
   * @return string
   *   'json' or 'yaml'.
   */
  #[\NoDiscard]
  protected function getConfiguredFormat(): string {
    $config = $this->configFactory->get('ffmpeg_media.settings');
    return $config->get('manifest_format') ?? self::DEFAULT_FORMAT;
  }

  /**
   * Builds manifest data from input path and options.
   *
   * @param string $inputPath
   *   Path to input file.
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return array<string, mixed>
   *   Manifest data.
   */
  protected function buildManifest(string $inputPath, array $options): array {
    $manifest = [
      'version' => '1.0',
      'generated' => date('c'),
      'generator' => 'ffmpeg_media',
      'input' => [
        'path' => $inputPath,
      ],
    ];

    // Add input file info if file exists.
    if (file_exists($inputPath)) {
      $size = filesize($inputPath);
      $manifest['input']['size'] = $size !== FALSE ? $size : NULL;
      $finfo = new \finfo(FILEINFO_MIME_TYPE);
      $mimeType = $finfo->file($inputPath);
      $manifest['input']['mime_type'] = $mimeType !== FALSE ? $mimeType : NULL;

      if ($options['include_checksums'] ?? FALSE) {
        $sha256 = hash_file('sha256', $inputPath);
        $manifest['input']['sha256'] = $sha256 !== FALSE ? $sha256 : NULL;
      }

      // Try to get video info.
      $videoInfo = $this->ffmpegExecutor->getVideoInfo($inputPath);
      if ($videoInfo !== NULL) {
        $manifest['metadata'] = [
          'duration' => $videoInfo->duration,
          'width' => $videoInfo->width,
          'height' => $videoInfo->height,
          'codec' => $videoInfo->codec,
          'format' => $videoInfo->format,
        ];
      }
    }

    // Add custom data from options.
    if (!empty($options['outputs'])) {
      $manifest['outputs'] = [];
      foreach ($options['outputs'] as $formatKey => $path) {
        $manifest['outputs'][$formatKey] = $this->buildFileInfo(
          $path,
          $options['include_sizes'] ?? TRUE,
          $options['include_checksums'] ?? FALSE,
        );
      }
    }

    if (!empty($options['thumbnail'])) {
      $manifest['thumbnail'] = $this->buildFileInfo(
        $options['thumbnail'],
        $options['include_sizes'] ?? TRUE,
        $options['include_checksums'] ?? FALSE,
      );
    }

    if (!empty($options['profile'])) {
      $manifest['processing']['profile'] = $options['profile'];
    }

    if (!empty($options['duration'])) {
      $manifest['processing']['duration_seconds'] = round((float) $options['duration'], 3);
    }

    return $manifest;
  }

  /**
   * Builds file information array.
   *
   * @param string $path
   *   File path.
   * @param bool $includeSize
   *   Whether to include file size.
   * @param bool $includeChecksum
   *   Whether to include MD5 checksum.
   *
   * @return array<string, int|float|string|null>
   *   File information.
   */
  protected function buildFileInfo(string $path, bool $includeSize, bool $includeChecksum): array {
    $info = ['path' => $path];

    if (file_exists($path)) {
      if ($includeSize) {
        $size = filesize($path);
        $info['size'] = $size !== FALSE ? $size : NULL;
      }
      if ($includeChecksum) {
        $sha256 = hash_file('sha256', $path);
        $info['sha256'] = $sha256 !== FALSE ? $sha256 : NULL;
      }
    }

    return $info;
  }

  /**
   * Formats manifest data to string.
   *
   * @param array<string, mixed> $manifest
   *   Manifest data.
   * @param string $format
   *   Output format ('json' or 'yaml').
   *
   * @return string|null
   *   Formatted content or NULL on error.
   */
  protected function formatManifest(array $manifest, string $format): ?string {
    if ($format === self::FORMAT_YAML) {
      return Yaml::dump($manifest, 4, 2, Yaml::DUMP_NULL_AS_TILDE);
    }

    $json = json_encode($manifest, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
    return $json !== FALSE ? $json : NULL;
  }

}
