<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Plugin\FFmpegOutput;

use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\ffmpeg_media\Trait\PluginDefinitionAccessTrait;
use Drupal\ffmpeg_media\ValueObject\ConversionOptions;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
 * Base class for FFmpeg output format plugins.
 */
abstract class FFmpegOutputBase extends PluginBase implements FFmpegOutputInterface, ContainerFactoryPluginInterface {

  use PluginDefinitionAccessTrait;

  /**
   * Constructs an FFmpegOutputBase object.
   *
   * @param array<string, mixed> $configuration
   *   Plugin configuration.
   * @param string $plugin_id
   *   Plugin ID.
   * @param mixed $plugin_definition
   *   Plugin definition.
   * @param \Drupal\Core\File\FileSystemInterface $fileSystem
   *   File system service.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    #[Autowire(service: 'file_system')]
    protected readonly FileSystemInterface $fileSystem,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getExtension(): string {
    return (string) $this->getDefinitionValue('extension', '');
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getMimeType(): string {
    return (string) $this->getDefinitionValue('mimeType', 'application/octet-stream');
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getLabel(): string {
    return (string) $this->getDefinitionValue('label', $this->pluginId);
  }

  /**
   * {@inheritdoc}
   *
   * @return array<int, string>
   *   Supported codec IDs.
   */
  #[\Override]
  #[\NoDiscard]
  public function getSupportedCodecs(): array {
    $codecs = $this->getDefinitionValue('supportedCodecs', []);
    if (!is_array($codecs)) {
      return [];
    }
    return $codecs;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getDefaultCodec(): string {
    return (string) $this->getDefinitionValue('defaultCodec', '');
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function supportsCodec(string $codecId): bool {
    $supported = $this->getSupportedCodecs();
    return empty($supported) || in_array($codecId, $supported, TRUE);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function isStreamingOutput(): bool {
    return FALSE;
  }

  /**
   * {@inheritdoc}
   *
   * @return array<int, string>
   *   Supported audio codec IDs.
   */
  #[\Override]
  #[\NoDiscard]
  public function getSupportedAudioCodecs(): array {
    $codecs = $this->getDefinitionValue('supportedAudioCodecs', []);
    if (!is_array($codecs)) {
      return [];
    }
    return $codecs;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function supportsAudioCodec(string $audioCodecId): bool {
    if (!$this->supportsAudio()) {
      return FALSE;
    }

    // 'copy' is always allowed when audio is supported.
    if ($audioCodecId === 'copy') {
      return TRUE;
    }

    $supported = $this->getSupportedAudioCodecs();
    return empty($supported) || in_array($audioCodecId, $supported, TRUE);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function supportsAudio(): bool {
    return (bool) $this->getDefinitionValue('supportsAudio', TRUE);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function generateOutputPath(string $inputPath): string {
    $pathInfo = pathinfo($inputPath);
    $baseName = $pathInfo['filename'];
    $directory = $pathInfo['dirname'] ?? '';

    $extension = $this->getExtension();
    $outputFileName = $baseName . '_converted.' . $extension;

    return $directory . DIRECTORY_SEPARATOR . $outputFileName;
  }

  /**
   * {@inheritdoc}
   *
   * @return array<int, string>
   *   FFmpeg arguments.
   */
  #[\Override]
  #[\NoDiscard]
  public function buildArguments(ConversionOptions $options): array {
    return [];
  }

  /**
   * Generates a unique output filename.
   *
   * Uses exclusive file creation to avoid TOCTOU race conditions when
   * parallel conversions target the same output directory.
   *
   * @param string $baseName
   *   Base filename without extension.
   * @param string $directory
   *   Target directory.
   *
   * @return string
   *   Unique output path.
   */
  protected function generateUniqueOutputPath(string $baseName, string $directory): string {
    $extension = $this->getExtension();
    $outputPath = $directory . DIRECTORY_SEPARATOR . $baseName . '.' . $extension;

    // Attempt atomic exclusive creation to prevent TOCTOU races.
    $realDirectory = $this->fileSystem->realpath($directory);
    if ($realDirectory !== FALSE) {
      $directory = $realDirectory;
      $outputPath = $directory . DIRECTORY_SEPARATOR . $baseName . '.' . $extension;
    }

    $counter = 0;
    while (TRUE) {
      $candidate = $counter === 0
        ? $outputPath
        : $directory . DIRECTORY_SEPARATOR . $baseName . '_' . $counter . '.' . $extension;

      // LOCK_EX with 'x' mode: fails atomically if file already exists.
      $handle = @fopen($candidate, 'x');
      if ($handle !== FALSE) {
        fclose($handle);
        return $candidate;
      }

      $counter++;

      // Safety limit to prevent infinite loops on unexpected filesystem state.
      if ($counter > 1000) {
        throw new \RuntimeException(sprintf(
          'Unable to generate unique output path after %d attempts in %s.',
          $counter,
          $directory,
        ));
      }
    }
  }

}
