<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Core\State\StateInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\Process\ExecutableFinder;

/**
 * Detects FFmpeg and FFprobe binary paths on the system.
 *
 * Uses Symfony's ExecutableFinder to scan PATH and common installation
 * directories across Linux, macOS (Homebrew, MacPorts), and Snap.
 *
 * @internal
 */
final class FFmpegPathDetector implements FFmpegPathDetectorInterface {

  /**
   * Extra directories to scan beyond PATH.
   *
   * @var array<int, string>
   */
  private const array EXTRA_DIRS = [
    '/usr/bin',
    '/usr/local/bin',
    '/opt/homebrew/bin',
    '/opt/local/bin',
    '/snap/bin',
    '/opt/ffmpeg/bin',
    '/opt/bin',
    '/run/current-system/sw/bin',
  ];

  /**
   * State key for last logged detection results.
   */
  private const string LAST_DETECTION_STATE_KEY = 'ffmpeg_media.path_detector.last_detections';

  /**
   * Internal marker for "not found" detection results.
   */
  private const string NOT_FOUND_MARKER = '__not_found__';

  /**
   * Constructs an FFmpegPathDetector object.
   *
   * @param \Psr\Log\LoggerInterface $logger
   *   The logger channel.
   * @param \Symfony\Component\Process\ExecutableFinder|null $executableFinder
   *   The executable finder. Injected for testability.
   * @param \Drupal\Core\State\StateInterface|null $state
   *   State service for suppressing duplicate detection logs across requests.
   */
  public function __construct(
    protected readonly LoggerInterface $logger,
    protected readonly ?ExecutableFinder $executableFinder = NULL,
    protected readonly ?StateInterface $state = NULL,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function detectFfmpegPath(): ?string {
    return $this->findBinary('ffmpeg');
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function detectFfprobePath(): ?string {
    return $this->findBinary('ffprobe');
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function detectPaths(): array {
    return [
      'ffmpeg' => $this->detectFfmpegPath(),
      'ffprobe' => $this->detectFfprobePath(),
    ];
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function isValidExecutable(string $path): bool {
    return $path !== '' && is_file($path) && is_executable($path);
  }

  /**
   * Finds a binary by name using ExecutableFinder.
   *
   * @param string $name
   *   The binary name (e.g., 'ffmpeg', 'ffprobe').
   *
   * @return string|null
   *   The absolute path, or NULL if not found.
   */
  protected function findBinary(string $name): ?string {
    $finder = $this->executableFinder ?? new ExecutableFinder();

    $path = $finder->find($name, NULL, self::EXTRA_DIRS);

    if ($this->shouldLogDetectionResult($name, $path)) {
      if ($path !== NULL) {
        $this->logger->debug('Auto-detected @name at @path.', [
          '@name' => $name,
          '@path' => $path,
        ]);
      }
      else {
        $this->logger->debug('Could not auto-detect @name.', [
          '@name' => $name,
        ]);
      }
    }

    return $path;
  }

  /**
   * Decides whether a detection result should be logged.
   *
   * Prevents repeated log spam for unchanged detection results.
   *
   * @param string $name
   *   Binary name (e.g., ffmpeg, ffprobe).
   * @param string|null $path
   *   Detected path or NULL if not found.
   *
   * @return bool
   *   TRUE if this result should be logged.
   */
  private function shouldLogDetectionResult(string $name, ?string $path): bool {
    $signature = $path ?? self::NOT_FOUND_MARKER;

    // Unit-test/runtime fallback when no state service is injected.
    if ($this->state === NULL) {
      static $runtimeDetections = [];

      if (($runtimeDetections[$name] ?? NULL) === $signature) {
        return FALSE;
      }

      $runtimeDetections[$name] = $signature;
      return TRUE;
    }

    $lastDetections = $this->state->get(self::LAST_DETECTION_STATE_KEY, []);
    if (!is_array($lastDetections)) {
      $lastDetections = [];
    }

    if (($lastDetections[$name] ?? NULL) === $signature) {
      return FALSE;
    }

    $lastDetections[$name] = $signature;
    $this->state->set(self::LAST_DETECTION_STATE_KEY, $lastDetections);

    return TRUE;
  }

}
