<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Core\State\StateInterface;
use Psr\Log\LoggerInterface;

/**
 * Resolves FFmpeg binary paths from config, auto-detection, or defaults.
 *
 * @internal
 */
final class FFmpegBinaryPathResolver implements FFmpegBinaryPathResolverInterface {

  /**
   * State key for last path resolution signatures.
   */
  private const string LAST_PATH_RESOLUTION_STATE_KEY = 'ffmpeg_media.executor.last_path_resolution';

  /**
   * Constructs an FFmpegBinaryPathResolver object.
   *
   * @param \Drupal\ffmpeg_media\Service\ConfigValueProviderInterface $configValueProvider
   *   Config value provider for cached config reads.
   * @param \Drupal\ffmpeg_media\Service\FFmpegPathDetectorInterface $pathDetector
   *   Path detector for auto-detection fallback.
   * @param \Psr\Log\LoggerInterface $logger
   *   Logger channel.
   * @param \Drupal\Core\State\StateInterface|null $state
   *   State service used to suppress repeated fallback warnings.
   */
  public function __construct(
    protected readonly ConfigValueProviderInterface $configValueProvider,
    protected readonly FFmpegPathDetectorInterface $pathDetector,
    protected readonly LoggerInterface $logger,
    protected readonly ?StateInterface $state = NULL,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getFfmpegPath(): string {
    return $this->resolvePath(
      binary: 'ffmpeg',
      configuredPath: $this->configValueProvider->getFfmpegPath(),
      detector: $this->pathDetector->detectFfmpegPath(...),
      fallbackPath: '/usr/bin/ffmpeg',
      fallbackWarning: 'FFmpeg path not configured and auto-detection failed, using default /usr/bin/ffmpeg.',
    );
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getFfprobePath(): string {
    return $this->resolvePath(
      binary: 'ffprobe',
      configuredPath: $this->configValueProvider->getFfprobePath(),
      detector: $this->pathDetector->detectFfprobePath(...),
      fallbackPath: '/usr/bin/ffprobe',
      fallbackWarning: 'FFprobe path not configured and auto-detection failed, using default /usr/bin/ffprobe.',
    );
  }

  /**
   * Resolves a configured, detected, or fallback binary path.
   *
   * @param string $binary
   *   Binary name (ffmpeg/ffprobe).
   * @param string $configuredPath
   *   Configured path or empty string.
   * @param \Closure(): ?string $detector
   *   Deferred detector callback.
   * @param string $fallbackPath
   *   Final hard-coded fallback path.
   * @param string $fallbackWarning
   *   Warning message logged when the fallback is used.
   *
   * @return string
   *   Resolved binary path.
   */
  private function resolvePath(
    string $binary,
    string $configuredPath,
    \Closure $detector,
    string $fallbackPath,
    string $fallbackWarning,
  ): string {
    if ($configuredPath !== '') {
      $this->trackPathResolution($binary, 'configured:' . $configuredPath);
      return $configuredPath;
    }

    $detectedPath = $detector();
    if ($detectedPath !== NULL) {
      $this->trackPathResolution($binary, 'detected:' . $detectedPath);
      return $detectedPath;
    }

    if ($this->shouldLogPathFallback($binary, $fallbackPath)) {
      $this->logger->warning($fallbackWarning);
    }

    return $fallbackPath;
  }

  /**
   * Determines whether fallback warning should be logged for a binary.
   *
   * @param string $binary
   *   Binary name (ffmpeg/ffprobe).
   * @param string $fallbackPath
   *   Fallback path being used.
   *
   * @return bool
   *   TRUE when this fallback transition should be logged.
   */
  private function shouldLogPathFallback(string $binary, string $fallbackPath): bool {
    return $this->trackPathResolution($binary, 'fallback:' . $fallbackPath);
  }

  /**
   * Tracks path resolution signatures and returns whether they changed.
   *
   * @param string $binary
   *   Binary name (ffmpeg/ffprobe).
   * @param string $signature
   *   Resolution signature, e.g. configured:/path, detected:/path.
   *
   * @return bool
   *   TRUE if the resolution signature changed.
   */
  private function trackPathResolution(string $binary, string $signature): bool {
    if ($this->state === NULL) {
      static $runtimeResolutions = [];

      if (($runtimeResolutions[$binary] ?? NULL) === $signature) {
        return FALSE;
      }

      $runtimeResolutions[$binary] = $signature;
      return TRUE;
    }

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

    if (($lastResolutions[$binary] ?? NULL) === $signature) {
      return FALSE;
    }

    $lastResolutions[$binary] = $signature;
    $this->state->set(self::LAST_PATH_RESOLUTION_STATE_KEY, $lastResolutions);
    return TRUE;
  }

}
