<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Hook;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Extension\Requirement\RequirementSeverity;
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\ffmpeg_media\Service\FFmpegExecutorInterface;
use Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface;
use Drupal\ffmpeg_media\Service\HardwareDetectorInterface;
use Drupal\ffmpeg_media\Service\QueueManagerInterface;

/**
 * Runtime requirements checks for FFmpeg Media.
 */
final class RequirementsHook {

  use StringTranslationTrait;

  /**
   * Queue size threshold that triggers a warning.
   */
  protected const int QUEUE_WARNING_THRESHOLD = 50;

  /**
   * Constructs a RequirementsHook object.
   *
   * @param \Drupal\ffmpeg_media\Service\FFmpegExecutorInterface $ffmpegExecutor
   *   The FFmpeg executor service.
   * @param \Drupal\ffmpeg_media\Service\HardwareDetectorInterface $hardwareDetector
   *   The hardware detector service.
   * @param \Drupal\ffmpeg_media\Service\QueueManagerInterface $queueManager
   *   The queue manager service.
   * @param \Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface $formatterSettingsResolver
   *   The formatter settings resolver service.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $streamWrapperManager
   *   The stream wrapper manager.
   * @param \Drupal\Core\State\StateInterface $state
   *   The state service for checking non-exportable secrets.
   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
   *   Logger channel.
   * @param object|null $keyRepository
   *   Optional Key module repository service.
   */
  public function __construct(
    protected readonly FFmpegExecutorInterface $ffmpegExecutor,
    protected readonly HardwareDetectorInterface $hardwareDetector,
    protected readonly QueueManagerInterface $queueManager,
    protected readonly FormatterSettingsResolverInterface $formatterSettingsResolver,
    protected readonly ConfigFactoryInterface $configFactory,
    protected readonly StreamWrapperManagerInterface $streamWrapperManager,
    protected readonly StateInterface $state,
    protected readonly LoggerChannelInterface $logger,
    protected readonly ?object $keyRepository = NULL,
  ) {}

  /**
   * Implements hook_runtime_requirements().
   *
   * @phpstan-return array<string, array<string, mixed>>
   */
  #[Hook('runtime_requirements')]
  public function runtimeRequirements(): array {
    $requirements = [];

    $requirements += $this->checkFfmpeg();
    $requirements += $this->checkFfprobe();
    $requirements += $this->checkFormatterConfiguration();
    $requirements += $this->checkOutputDirectory();
    $requirements += $this->checkTrashPath();
    $requirements += $this->checkHardwareAcceleration();
    $requirements += $this->checkQueueStatus();
    $requirements += $this->checkPrivateFileSystem();
    $requirements += $this->checkCdnSecrets();

    return $requirements;
  }

  /**
   * Checks FFmpeg binary availability and version.
   *
   * @phpstan-return array<string, array<string, mixed>>
   */
  protected function checkFfmpeg(): array {
    $ffmpegPath = $this->ffmpegExecutor->getFfmpegPath();

    if (!$this->ffmpegExecutor->isAvailable()) {
      $settingsUrl = Url::fromRoute('ffmpeg_media.settings')->toString();

      return [
        'ffmpeg_media_ffmpeg' => [
          'title' => $this->t('FFmpeg'),
          'value' => $this->t('Not found'),
          'severity' => RequirementSeverity::Error,
          'description' => $this->t('FFmpeg binary not found or not executable at %path. Install FFmpeg or configure the correct path at <a href="@url">FFmpeg Media Settings</a>.', [
            '%path' => $ffmpegPath,
            '@url' => $settingsUrl,
          ]),
        ],
      ];
    }

    $version = $this->ffmpegExecutor->getVersion();

    if ($version === NULL) {
      return [
        'ffmpeg_media_ffmpeg' => [
          'title' => $this->t('FFmpeg'),
          'value' => $this->t('Unknown version'),
          'severity' => RequirementSeverity::Warning,
          'description' => $this->t('FFmpeg binary found at %path but version could not be determined.', [
            '%path' => $ffmpegPath,
          ]),
        ],
      ];
    }

    return [
      'ffmpeg_media_ffmpeg' => [
        'title' => $this->t('FFmpeg'),
        'value' => $version,
        'severity' => RequirementSeverity::OK,
      ],
    ];
  }

  /**
   * Checks ffprobe binary availability.
   *
   * @phpstan-return array<string, array<string, mixed>>
   */
  protected function checkFfprobe(): array {
    $ffprobePath = $this->ffmpegExecutor->getFfprobePath();

    if (!is_executable($ffprobePath)) {
      return [
        'ffmpeg_media_ffprobe' => [
          'title' => $this->t('ffprobe'),
          'value' => $this->t('Not found'),
          'severity' => RequirementSeverity::Warning,
          'description' => $this->t('ffprobe binary not found or not executable at %path. Video metadata analysis will not be available. Install ffprobe or configure the correct path.', [
            '%path' => $ffprobePath,
          ]),
        ],
      ];
    }

    return [
      'ffmpeg_media_ffprobe' => [
        'title' => $this->t('ffprobe'),
        'value' => $ffprobePath,
        'severity' => RequirementSeverity::OK,
      ],
    ];
  }

  /**
   * Checks formatter configuration.
   *
   * @phpstan-return array<string, array<string, mixed>>
   */
  protected function checkFormatterConfiguration(): array {
    if ($this->formatterSettingsResolver->hasAnyConfiguration()) {
      $contexts = $this->formatterSettingsResolver->getAllContexts();
      return [
        'ffmpeg_media_formatter_configuration' => [
          'title' => $this->t('FFmpeg Media formatter configuration'),
          'value' => $this->t('@count formatter(s) configured', ['@count' => count($contexts)]),
          'severity' => RequirementSeverity::OK,
        ],
      ];
    }

    return [
      'ffmpeg_media_formatter_configuration' => [
        'title' => $this->t('FFmpeg Media formatter configuration'),
        'value' => $this->t('Not configured'),
        'description' => $this->t('No video formatter configured. Configure a video formatter under Manage Display for your media type to start converting videos.'),
        'severity' => RequirementSeverity::Warning,
      ],
    ];
  }

  /**
   * Checks output directory validity and stream wrapper availability.
   *
   * @phpstan-return array<string, array<string, mixed>>
   */
  protected function checkOutputDirectory(): array {
    $outputDirectory = (string) ($this->configFactory->get('ffmpeg_media.settings')->get('output_directory') ?? '');
    if ($outputDirectory === '') {
      return [];
    }

    $settingsUrl = Url::fromRoute('ffmpeg_media.settings')->toString();

    if (!str_contains($outputDirectory, '://')) {
      return [
        'ffmpeg_media_output_directory' => [
          'title' => $this->t('FFmpeg Media output directory'),
          'value' => $this->t('Invalid filesystem path'),
          'description' => $this->t('The configured output directory %path is a filesystem path, but FFmpeg Media requires a Drupal stream wrapper URI such as <code>public://ffmpeg_media</code> or <code>private://ffmpeg_media</code>. Update the setting at <a href="@url">FFmpeg Media Settings</a>.', [
            '%path' => $outputDirectory,
            '@url' => $settingsUrl,
          ]),
          'severity' => RequirementSeverity::Warning,
        ],
      ];
    }

    $scheme = explode('://', $outputDirectory, 2)[0];
    if ($this->streamWrapperManager->getViaScheme($scheme)) {
      return [];
    }

    return [
      'ffmpeg_media_output_directory' => [
        'title' => $this->t('FFmpeg Media output directory'),
        'value' => $this->t('Stream wrapper @scheme:// not available', ['@scheme' => $scheme]),
        'description' => $this->t('The configured output directory uses the @scheme:// stream wrapper, but it is not available in this environment. Change the setting at <a href="@url">FFmpeg Media Settings</a> or configure the @scheme file system.', [
          '@scheme' => $scheme,
          '@url' => $settingsUrl,
        ]),
        'severity' => RequirementSeverity::Warning,
      ],
    ];
  }

  /**
   * Checks trash path stream wrapper availability.
   *
   * @phpstan-return array<string, array<string, mixed>>
   */
  protected function checkTrashPath(): array {
    $trashPath = $this->configFactory->get('ffmpeg_media.settings')->get('original_trash_path') ?? '';

    if ($trashPath === '' || !str_contains($trashPath, '://')) {
      return [];
    }

    $scheme = explode('://', $trashPath, 2)[0];

    if ($this->streamWrapperManager->getViaScheme($scheme)) {
      return [];
    }

    $settingsUrl = Url::fromRoute('ffmpeg_media.settings')->toString();

    return [
      'ffmpeg_media_trash_path' => [
        'title' => $this->t('FFmpeg Media trash directory'),
        'value' => $this->t('Stream wrapper @scheme:// not available', ['@scheme' => $scheme]),
        'description' => $this->t('The configured trash directory uses the @scheme:// stream wrapper which is not available. Change the trash path at <a href="@url">FFmpeg Media Settings</a> or configure the @scheme file system.', [
          '@scheme' => $scheme,
          '@url' => $settingsUrl,
        ]),
        'severity' => RequirementSeverity::Warning,
      ],
    ];
  }

  /**
   * Checks hardware acceleration availability.
   *
   * @phpstan-return array<string, array<string, mixed>>
   */
  protected function checkHardwareAcceleration(): array {
    $hardware = $this->hardwareDetector->detectAvailableHardware();

    if ($hardware === []) {
      return [
        'ffmpeg_media_hardware' => [
          'title' => $this->t('FFmpeg hardware acceleration'),
          'value' => $this->t('None detected'),
          'severity' => RequirementSeverity::Info,
          'description' => $this->t('No hardware acceleration detected. Software encoding will be used. This is normal for environments without a GPU.'),
        ],
      ];
    }

    return [
      'ffmpeg_media_hardware' => [
        'title' => $this->t('FFmpeg hardware acceleration'),
        'value' => implode(', ', $hardware),
        'severity' => RequirementSeverity::Info,
      ],
    ];
  }

  /**
   * Checks conversion queue status.
   *
   * @phpstan-return array<string, array<string, mixed>>
   */
  protected function checkQueueStatus(): array {
    $stats = $this->queueManager->getQueueStats();
    $pending = $stats['pending'];

    if ($pending > self::QUEUE_WARNING_THRESHOLD) {
      return [
        'ffmpeg_media_queue' => [
          'title' => $this->t('FFmpeg Media queue'),
          'value' => $this->t('@count items pending', ['@count' => $pending]),
          'severity' => RequirementSeverity::Warning,
          'description' => $this->t('The video conversion queue has @count pending items, which exceeds the threshold of @threshold. Consider running cron more frequently or increasing the number of concurrent conversions.', [
            '@count' => $pending,
            '@threshold' => self::QUEUE_WARNING_THRESHOLD,
          ]),
        ],
      ];
    }

    return [
      'ffmpeg_media_queue' => [
        'title' => $this->t('FFmpeg Media queue'),
        'value' => $this->t('@count items pending', ['@count' => $pending]),
        'severity' => RequirementSeverity::OK,
      ],
    ];
  }

  /**
   * Checks private file system availability for thumbnail access control.
   *
   * @phpstan-return array<string, array<string, mixed>>
   */
  protected function checkPrivateFileSystem(): array {
    if ($this->streamWrapperManager->getViaScheme('private') !== FALSE) {
      return [
        'ffmpeg_media_private_files' => [
          'title' => $this->t('FFmpeg Media private file system'),
          'value' => $this->t('Available'),
          'severity' => RequirementSeverity::OK,
        ],
      ];
    }

    return [
      'ffmpeg_media_private_files' => [
        'title' => $this->t('FFmpeg Media private file system'),
        'value' => $this->t('Not configured'),
        'severity' => RequirementSeverity::Warning,
        'description' => $this->t("The private file system is not configured. Temporary thumbnails and previews generated during poster selection will be stored in public:// without Drupal access control. Configure <code>\$settings['file_private_path']</code> in settings.php to enable access-controlled file serving."),
      ],
    ];
  }

  /**
   * Checks CDN S3 secret storage for security best practices.
   *
   * Warns when secrets are stored in State API without environment variables,
   * and errors when legacy secrets are found in exportable config.
   *
   * @phpstan-return array<string, array<string, mixed>>
   */
  protected function checkCdnSecrets(): array {
    $config = $this->configFactory->get('ffmpeg_media.cdn');
    if (!$config->get('enabled')) {
      return [];
    }

    $envAccessKey = getenv('FFMPEG_MEDIA_S3_ACCESS_KEY');
    $envSecretKey = getenv('FFMPEG_MEDIA_S3_SECRET_KEY');

    // Check for legacy secrets still in exportable config.
    $configAccessKey = $config->get('s3.access_key') ?? '';
    $configSecretKey = $config->get('s3.secret_key') ?? '';
    $hasConfigSecrets = ($configAccessKey !== '' || $configSecretKey !== '');

    if ($hasConfigSecrets) {
      $settingsUrl = Url::fromRoute('ffmpeg_media.cdn_settings')->toString();
      return [
        'ffmpeg_media_cdn_secrets' => [
          'title' => $this->t('FFmpeg Media CDN credentials'),
          'value' => $this->t('Secrets in exportable config'),
          'severity' => RequirementSeverity::Error,
          'description' => $this->t('S3 credentials are stored in exportable Drupal config. They will be included in <code>drush config:export</code>. Re-save the <a href="@url">CDN settings form</a> to migrate them to the State API, or set FFMPEG_MEDIA_S3_ACCESS_KEY and FFMPEG_MEDIA_S3_SECRET_KEY environment variables.', [
            '@url' => $settingsUrl,
          ]),
        ],
      ];
    }

    $accessKeyRef = trim((string) ($config->get('s3.access_key_key') ?? ''));
    $secretKeyRef = trim((string) ($config->get('s3.secret_key_key') ?? ''));
    $hasAccessRef = $accessKeyRef !== '';
    $hasSecretRef = $secretKeyRef !== '';
    if ($hasAccessRef || $hasSecretRef) {
      if (!$this->hasKeyRepository()) {
        return [
          'ffmpeg_media_cdn_secrets' => [
            'title' => $this->t('FFmpeg Media CDN credentials'),
            'value' => $this->t('Key references configured, Key module missing'),
            'severity' => RequirementSeverity::Warning,
            'description' => $this->t('CDN credential references are configured but the Key module service is unavailable. Install/enable Key module or remove key references in CDN settings.'),
          ],
        ];
      }

      if ($hasAccessRef && $hasSecretRef) {
        return [
          'ffmpeg_media_cdn_secrets' => [
            'title' => $this->t('FFmpeg Media CDN credentials'),
            'value' => $this->t('Key module references'),
            'severity' => RequirementSeverity::OK,
          ],
        ];
      }

      return [
        'ffmpeg_media_cdn_secrets' => [
          'title' => $this->t('FFmpeg Media CDN credentials'),
          'value' => $this->t('Incomplete key references'),
          'severity' => RequirementSeverity::Warning,
          'description' => $this->t('Configure both access and secret key references for Key module based credential loading.'),
        ],
      ];
    }

    // Both env vars set: ideal state.
    if (($envAccessKey !== FALSE && $envAccessKey !== '')
      && ($envSecretKey !== FALSE && $envSecretKey !== '')) {
      return [
        'ffmpeg_media_cdn_secrets' => [
          'title' => $this->t('FFmpeg Media CDN credentials'),
          'value' => $this->t('Environment variables'),
          'severity' => RequirementSeverity::OK,
        ],
      ];
    }

    // Secrets in State but not env vars: acceptable but warn for production.
    $stateAccessKey = $this->state->get('ffmpeg_media.s3_access_key');
    $stateSecretKey = $this->state->get('ffmpeg_media.s3_secret_key');
    if (!empty($stateAccessKey) || !empty($stateSecretKey)) {
      return [
        'ffmpeg_media_cdn_secrets' => [
          'title' => $this->t('FFmpeg Media CDN credentials'),
          'value' => $this->t('State API (not exported)'),
          'severity' => RequirementSeverity::Warning,
          'description' => $this->t('S3 credentials are stored in the State API (safe from config export). For production, set FFMPEG_MEDIA_S3_ACCESS_KEY and FFMPEG_MEDIA_S3_SECRET_KEY environment variables for maximum security.'),
        ],
      ];
    }

    return [];
  }

  /**
   * Checks whether Key module repository integration is available.
   */
  private function hasKeyRepository(): bool {
    if ($this->keyRepository === NULL) {
      $this->logger->debug('Key module not available: key.repository service not injected. CDN credential management limited to environment variables and State API.');
      return FALSE;
    }
    return method_exists($this->keyRepository, 'getKey');
  }

}
