<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media_advanced\Plugin\FFmpegProcessor;

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

/**
 * Processor plugin for adding watermarks to video.
 *
 * Features:
 * - Image watermark overlay (PNG with transparency recommended).
 * - Text watermark with customizable font, size, color.
 * - Configurable position (corners, center, custom coordinates).
 * - Opacity control for subtle branding.
 * - Scale watermark relative to video dimensions.
 */
#[FFmpegProcessor(
  id: 'watermark',
  label: new TranslatableMarkup('Watermark'),
  description: new TranslatableMarkup('Add image or text watermarks to video.'),
  outputType: 'video',
  outputExtension: 'mp4',
  requiredFilters: ['overlay', 'scale'],
  weight: 20,
)]
final class WatermarkProcessor extends FFmpegProcessorBase implements ContainerFactoryPluginInterface {

  /**
   * Available watermark positions.
   */
  public const array POSITIONS = [
    'top_left' => 'Top Left',
    'top_right' => 'Top Right',
    'bottom_left' => 'Bottom Left',
    'bottom_right' => 'Bottom Right',
    'center' => 'Center',
    'custom' => 'Custom Coordinates',
  ];

  /**
   * Constructs a WatermarkProcessor object.
   *
   * @param array $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
   *   File system service.
   */
  public function __construct(
    array $configuration,
    $plugin_id,
    $plugin_definition,
    #[Autowire(service: FFmpegExecutorInterface::class)]
    FFmpegExecutorInterface $ffmpegExecutor,
    protected readonly ConfigFactoryInterface $configFactory,
    protected readonly FileSystemInterface $fileSystem,
  ) {
    parent::__construct($configuration, $plugin_id, $plugin_definition, $ffmpegExecutor);
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function process(string $inputPath, string $outputPath, array $options = []): bool {
    $options = $this->mergeOptions($options);

    // Validate watermark source exists.
    if (!empty($options['image_path'])) {
      $imagePath = $this->fileSystem->realpath($options['image_path']);
      if ($imagePath === FALSE || !file_exists($imagePath)) {
        return FALSE;
      }
      return $this->processImageWatermark($inputPath, $outputPath, $imagePath, $options);
    }

    if (!empty($options['text'])) {
      return $this->processTextWatermark($inputPath, $outputPath, $options);
    }

    // No watermark configured, just copy.
    return FALSE;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getDefaultOptions(): array {
    $config = $this->configFactory->get('ffmpeg_media_advanced.settings');

    return [
      'image_path' => $config->get('watermark_image_path') ?? '',
      'text' => $config->get('watermark_text') ?? '',
      'position' => $config->get('watermark_position') ?? 'bottom_right',
      'opacity' => $config->get('watermark_opacity') ?? 0.7,
      'scale' => $config->get('watermark_scale') ?? 0.15,
      'margin' => $config->get('watermark_margin') ?? 10,
      'x' => 0,
      'y' => 0,
      'font_size' => $config->get('watermark_font_size') ?? 24,
      'font_color' => $config->get('watermark_font_color') ?? 'white',
      'font_file' => $config->get('watermark_font_file') ?? '',
      'video_codec' => 'libsvtav1',
      'crf' => 23,
      'audio_codec' => 'libopus',
      'audio_bitrate' => '128k',
      'timeout' => 3600,
    ];
  }

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

    return $directory . DIRECTORY_SEPARATOR . $baseName . '_watermarked.mp4';
  }

  /**
   * Processes video with image watermark overlay.
   *
   * @param string $input
   *   Input video path.
   * @param string $output
   *   Output video path.
   * @param string $imagePath
   *   Watermark image path.
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return bool
   *   TRUE if processing succeeded.
   */
  protected function processImageWatermark(
    string $input,
    string $output,
    string $imagePath,
    array $options,
  ): bool {
    $ffmpegPath = $this->ffmpegExecutor->getFfmpegPath();
    $opacity = (float) ($options['opacity'] ?? 0.7);
    $scale = (float) ($options['scale'] ?? 0.15);

    // Build overlay position expression.
    $position = $this->buildPositionExpression($options);

    // Build filter complex for watermark with scaling and opacity.
    $filterComplex = sprintf(
      '[1:v]scale=iw*%s:-1,format=rgba,colorchannelmixer=aa=%s[wm];[0:v][wm]overlay=%s',
      number_format($scale, 2),
      number_format($opacity, 2),
      $position,
    );

    // Build command array.
    $command = [
      $ffmpegPath,
      '-y',
      '-i', $input,
      '-i', $imagePath,
      '-filter_complex', $filterComplex,
      '-c:v', $options['video_codec'] ?? 'libsvtav1',
      '-crf', (string) ($options['crf'] ?? 23),
      '-c:a', $options['audio_codec'] ?? 'libopus',
      '-b:a', $options['audio_bitrate'] ?? '128k',
      '-movflags', '+faststart',
      $output,
    ];

    return $this->ffmpegExecutor->execute($command, [
      'timeout' => $options['timeout'] ?? 3600,
    ]);
  }

  /**
   * Processes video with text watermark.
   *
   * @param string $input
   *   Input video path.
   * @param string $output
   *   Output video path.
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return bool
   *   TRUE if processing succeeded.
   */
  protected function processTextWatermark(string $input, string $output, array $options): bool {
    $ffmpegPath = $this->ffmpegExecutor->getFfmpegPath();

    // Escape text for FFmpeg drawtext filter.
    // Backslash must be escaped first to avoid double-escaping the
    // backslashes introduced by subsequent replacements.
    $text = str_replace(
      ['\\', ':', "'", '%', ';', '[', ']'],
      ['\\\\', '\\:', "\\'", '%%', '\\;', '\\[', '\\]'],
      $options['text'] ?? '',
    );
    $fontSize = (int) ($options['font_size'] ?? 24);
    $fontColor = $this->sanitizeFontColor($options['font_color'] ?? 'white');
    $opacity = (float) ($options['opacity'] ?? 0.7);

    // Build position for drawtext.
    $textPosition = $this->buildTextPosition($options);

    // Build drawtext filter.
    $drawtext = sprintf(
      "drawtext=text='%s':fontsize=%d:fontcolor=%s@%s:%s",
      $text,
      $fontSize,
      $fontColor,
      number_format($opacity, 2),
      $textPosition,
    );

    // Add font file if specified (escape for drawtext filter parser).
    if (!empty($options['font_file'])) {
      $fontPath = $this->fileSystem->realpath($options['font_file']);
      if ($fontPath !== FALSE && file_exists($fontPath)) {
        $escapedFontPath = str_replace(
          ['\\', ':', "'", '%', ';', '[', ']'],
          ['\\\\', '\\:', "\\'", '%%', '\\;', '\\[', '\\]'],
          $fontPath,
        );
        $drawtext .= ':fontfile=' . $escapedFontPath;
      }
    }

    // Build command array.
    $command = [
      $ffmpegPath,
      '-y',
      '-i', $input,
      '-vf', $drawtext,
      '-c:v', $options['video_codec'] ?? 'libsvtav1',
      '-crf', (string) ($options['crf'] ?? 23),
      '-c:a', $options['audio_codec'] ?? 'libopus',
      '-b:a', $options['audio_bitrate'] ?? '128k',
      '-movflags', '+faststart',
      $output,
    ];

    return $this->ffmpegExecutor->execute($command, [
      'timeout' => $options['timeout'] ?? 3600,
    ]);
  }

  /**
   * Builds overlay position expression for image watermark.
   *
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return string
   *   FFmpeg overlay position expression.
   */
  protected function buildPositionExpression(array $options): string {
    $position = $options['position'] ?? 'bottom_right';
    $margin = (int) ($options['margin'] ?? 10);

    return match ($position) {
      'top_left' => sprintf('%d:%d', $margin, $margin),
      'top_right' => sprintf('W-w-%d:%d', $margin, $margin),
      'bottom_left' => sprintf('%d:H-h-%d', $margin, $margin),
      'bottom_right' => sprintf('W-w-%d:H-h-%d', $margin, $margin),
      'center' => '(W-w)/2:(H-h)/2',
      'custom' => sprintf('%d:%d', $options['x'] ?? 0, $options['y'] ?? 0),
      default => sprintf('W-w-%d:H-h-%d', $margin, $margin),
    };
  }

  /**
   * Builds position expression for text watermark.
   *
   * @param array<string, mixed> $options
   *   Processing options.
   *
   * @return string
   *   FFmpeg drawtext position expression.
   */
  protected function buildTextPosition(array $options): string {
    $position = $options['position'] ?? 'bottom_right';
    $margin = (int) ($options['margin'] ?? 10);

    return match ($position) {
      'top_left' => sprintf('x=%d:y=%d', $margin, $margin),
      'top_right' => sprintf('x=w-tw-%d:y=%d', $margin, $margin),
      'bottom_left' => sprintf('x=%d:y=h-th-%d', $margin, $margin),
      'bottom_right' => sprintf('x=w-tw-%d:y=h-th-%d', $margin, $margin),
      'center' => 'x=(w-tw)/2:y=(h-th)/2',
      'custom' => sprintf('x=%d:y=%d', $options['x'] ?? 0, $options['y'] ?? 0),
      default => sprintf('x=w-tw-%d:y=h-th-%d', $margin, $margin),
    };
  }

  /**
   * Gets available watermark positions.
   *
   * @return array<string, string>
   *   Position ID => description.
   */
  #[\NoDiscard]
  public static function getAvailablePositions(): array {
    return self::POSITIONS;
  }

  /**
   * Sanitizes a font color value for safe use in FFmpeg drawtext.
   *
   * Accepts named FFmpeg colors and hex codes. Rejects values containing
   * characters that could break or escape the filter graph.
   *
   * @param string $color
   *   Raw color value from configuration.
   *
   * @return string
   *   Sanitized color or 'white' as safe fallback.
   */
  public static function sanitizeFontColor(string $color): string {
    $color = trim($color);

    // Named colors: only word characters.
    if (preg_match('/^[a-zA-Z]+$/', $color)) {
      return $color;
    }

    // Hex color: #RRGGBB or #RRGGBBAA.
    $normalized = ltrim($color, '#');
    if (preg_match('/^[0-9A-Fa-f]{6}([0-9A-Fa-f]{2})?$/', $normalized)) {
      return '#' . $normalized;
    }

    // Reject anything else to prevent filter graph injection.
    return 'white';
  }

}
