<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Commands;

use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Queue\QueueFactory;
use Drupal\ffmpeg_media\Service\FFmpegExecutorInterface;
use Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface;
use Drupal\ffmpeg_media\Service\OutputStorageServiceInterface;
use Drupal\ffmpeg_media\Service\ProgressTrackerInterface;
use Drupal\ffmpeg_media\Service\QueueManagerInterface;
use Drush\Attributes as CLI;
use Drush\Commands\DrushCommands;
use Symfony\Component\Process\Process;

/**
 * Drush commands for FFmpeg Media system status and monitoring.
 */
final class SystemCommands extends DrushCommands {

  use CommandHelperTrait;

  /**
   * Constructs a SystemCommands instance.
   *
   * @param \Drupal\ffmpeg_media\Service\FFmpegExecutorInterface $ffmpegExecutor
   *   FFmpeg executor service.
   * @param \Drupal\ffmpeg_media\Service\QueueManagerInterface $queueManager
   *   Queue manager service.
   * @param \Drupal\ffmpeg_media\Service\ProgressTrackerInterface $progressTracker
   *   Progress tracker service.
   * @param \Drupal\ffmpeg_media\Service\FormatterSettingsResolverInterface $formatterSettingsResolver
   *   Formatter settings resolver service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   Entity type manager.
   * @param \Drupal\Core\Queue\QueueFactory $queueFactory
   *   Queue factory.
   * @param \Drupal\ffmpeg_media\Service\OutputStorageServiceInterface $outputStorage
   *   Output storage service.
   */
  public function __construct(
    protected readonly FFmpegExecutorInterface $ffmpegExecutor,
    protected readonly QueueManagerInterface $queueManager,
    protected readonly ProgressTrackerInterface $progressTracker,
    protected readonly FormatterSettingsResolverInterface $formatterSettingsResolver,
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly QueueFactory $queueFactory,
    protected readonly OutputStorageServiceInterface $outputStorage,
  ) {
    parent::__construct();
  }

  /**
   * Show system status including FFmpeg version and queue counts.
   */
  #[CLI\Command(name: 'ffmpeg:status', aliases: ['ffs'])]
  #[CLI\Usage(name: 'ffmpeg:status', description: 'Show FFmpeg Media system status.')]
  public function showStatus(): void {
    $this->io()->title('FFmpeg Media Status');

    // FFmpeg status.
    $ffmpegAvailable = $this->ffmpegExecutor->isAvailable();
    $ffmpegVersion = $this->ffmpegExecutor->getVersion();

    $this->io()->section('FFmpeg');
    $this->io()->listing([
      'Available: ' . ($ffmpegAvailable ? 'Yes' : 'No'),
      'Version: ' . ($ffmpegVersion ?? 'N/A'),
      'Path: ' . $this->ffmpegExecutor->getFfmpegPath(),
      'FFprobe Path: ' . $this->ffmpegExecutor->getFfprobePath(),
    ]);

    // Check common encoders.
    $encoders = ['libx264', 'libx265', 'libsvtav1', 'libvpx-vp9', 'libopus', 'aac'];
    $availableEncoders = [];
    foreach ($encoders as $encoder) {
      if ($this->ffmpegExecutor->hasEncoder($encoder)) {
        $availableEncoders[] = $encoder;
      }
    }
    $this->io()->text('Available Encoders: ' . (empty($availableEncoders) ? 'None detected' : implode(', ', $availableEncoders)));

    // Queue status.
    $queueStats = $this->queueManager->getQueueStats();
    $normalQueue = $this->queueFactory->get(QueueManagerInterface::QUEUE_NAME);
    $largeQueue = $this->queueFactory->get(QueueManagerInterface::QUEUE_LARGE_NAME);

    $this->io()->section('Conversion Queue');
    $this->io()->listing([
      'Queued Items: ' . $this->queueManager->getQueuedMediaCount(),
      'Normal Queue Backend: ' . $normalQueue->numberOfItems() . ' items',
      'Large File Queue Backend: ' . $largeQueue->numberOfItems() . ' items',
      'Currently Processing: ' . $queueStats['processing'],
      'Oldest Item: ' . ($queueStats['oldest_item'] ? date('Y-m-d H:i:s', $queueStats['oldest_item']) : 'N/A'),
    ]);

    // Get all contexts to find media to count.
    $contexts = $this->formatterSettingsResolver->getAllContexts();
    $totalVideos = 0;
    $convertedVideos = 0;
    $convertedMediaIds = $this->outputStorage->getConvertedMediaIds();

    foreach ($contexts as $context) {
      // Drush: admin context, access checks not needed.
      $query = $this->entityTypeManager->getStorage('media')->getQuery()
        ->condition('bundle', $context->bundle)
        ->accessCheck(FALSE);

      $bundleMediaIds = array_values($query->execute());
      $totalVideos += count($bundleMediaIds);

      // Count converted (have at least one output in the outputs table).
      $convertedVideos += count(array_intersect($bundleMediaIds, $convertedMediaIds));
    }

    $this->io()->section('Video Statistics');
    $this->io()->listing([
      'Total Video Media: ' . $totalVideos,
      'Converted Videos: ' . $convertedVideos,
      'Pending Conversion: ' . max(0, $totalVideos - $convertedVideos),
    ]);

    // Active conversions.
    $active = $this->progressTracker->getActiveConversions();
    if (!empty($active)) {
      $this->io()->section('Active Conversions');
      $rows = [];
      foreach ($active as $progress) {
        $rows[] = [
          $progress->mediaId,
          $progress->status,
          $progress->percent . '%',
          $progress->currentStep ?? '-',
          $this->formatDuration($progress->elapsedTime ?? 0),
        ];
      }
      $this->io()->table(['Media ID', 'Status', 'Progress', 'Step', 'Elapsed'], $rows);
    }
  }

  /**
   * Show active conversion progress.
   *
   * @param int|null $media_id
   *   Optional media ID to show progress for.
   */
  #[CLI\Command(name: 'ffmpeg:progress', aliases: ['ffpg'])]
  #[CLI\Argument(name: 'media_id', description: 'Optional media ID to show progress for.')]
  #[CLI\Usage(name: 'ffmpeg:progress', description: 'Show all active conversion progress.')]
  #[CLI\Usage(name: 'ffmpeg:progress 123', description: 'Show progress for media 123.')]
  public function showProgress(?int $media_id = NULL): void {
    if ($media_id !== NULL) {
      $progress = $this->progressTracker->getProgress($media_id);
      if ($progress === NULL) {
        $this->logger()?->notice('No progress record found for media {id}.', ['id' => $media_id]);
        return;
      }

      $this->io()->title("Conversion Progress for Media {$media_id}");
      $this->io()->definitionList(
        ['Status' => $progress->status],
        ['Overall Progress' => $progress->percent . '%'],
        ['Current Step' => $progress->currentStep ?? '-'],
        ['Current Output' => $progress->currentOutput ?? '-'],
        ['Started' => $progress->startedAt ? date('Y-m-d H:i:s', $progress->startedAt) : '-'],
        ['Completed' => $progress->completedAt ? date('Y-m-d H:i:s', $progress->completedAt) : '-'],
        ['Elapsed Time' => $this->formatDuration($progress->elapsedTime ?? 0)],
        ['Estimated Remaining' => $this->formatDuration($progress->estimatedRemainingTime ?? 0)],
      );

      if ($progress->error) {
        $this->io()->error($progress->error);
      }
    }
    else {
      $active = $this->progressTracker->getActiveConversions();

      if (empty($active)) {
        $this->logger()?->notice('No active conversions.');
        return;
      }

      $this->io()->title('Active Conversions');
      $rows = [];
      foreach ($active as $progress) {
        $elapsed = $progress->elapsedTime ?? 0;
        $rows[] = [
          $progress->mediaId,
          $progress->status,
          $this->formatProgressBar($progress->percent),
          $progress->currentStep ?? '-',
          $this->formatDuration($elapsed),
        ];
      }
      $this->io()->table(['Media ID', 'Status', 'Progress', 'Step', 'Elapsed'], $rows);
    }
  }

  /**
   * Show running FFmpeg processes.
   */
  #[CLI\Command(name: 'ffmpeg:processes', aliases: ['ffpr'])]
  #[CLI\Usage(name: 'ffmpeg:processes', description: 'Show all running FFmpeg conversion processes.')]
  public function showProcesses(): void {
    $processes = $this->getRunningFfmpegProcesses();

    if (empty($processes)) {
      $this->io()->success('No FFmpeg conversion processes running.');
      return;
    }

    $this->io()->title('Running FFmpeg Processes');

    $rows = [];
    foreach ($processes as $p) {
      $rows[] = [
        $p['pid'],
        $p['cpu'] . '%',
        $p['memory_mb'] . ' MB',
        $p['memory_percent'] . '%',
        $p['time'],
        mb_substr($p['command'], 0, 60) . '...',
      ];
    }

    $this->io()->table(['PID', 'CPU', 'Memory', 'Mem%', 'Time', 'Command'], $rows);
  }

  /**
   * Kill stuck FFmpeg processes that exceed limits.
   *
   * @param array $options
   *   Command options.
   *
   * @phpstan-param array{memory: int, time: int, dry-run: bool} $options
   */
  #[CLI\Command(name: 'ffmpeg:kill-stuck', aliases: ['ffks'])]
  #[CLI\Option(name: 'memory', description: 'Maximum memory in MB (default: 800).')]
  #[CLI\Option(name: 'time', description: 'Maximum runtime in minutes (default: 60).')]
  #[CLI\Option(name: 'dry-run', description: 'Show what would be killed without killing.')]
  #[CLI\Usage(name: 'ffmpeg:kill-stuck', description: 'Kill FFmpeg processes using >800MB or running >60min.')]
  #[CLI\Usage(name: 'ffmpeg:kill-stuck --memory=500', description: 'Kill processes using more than 500MB.')]
  #[CLI\Usage(name: 'ffmpeg:kill-stuck --dry-run', description: 'Show what would be killed.')]
  public function killStuck(array $options = ['memory' => 800, 'time' => 60, 'dry-run' => FALSE]): void {
    $maxMemory = (int) $options['memory'];
    $maxTime = (int) $options['time'];
    $dryRun = (bool) $options['dry-run'];

    $processes = $this->getRunningFfmpegProcesses();

    if (empty($processes)) {
      $this->io()->success('No FFmpeg processes running.');
      return;
    }

    $toKill = [];
    foreach ($processes as $p) {
      $reason = NULL;

      if ($p['memory_mb'] > $maxMemory) {
        $reason = "Memory {$p['memory_mb']}MB exceeds {$maxMemory}MB limit";
      }
      elseif ($p['time']) {
        $runtime = $this->parseRuntimeMinutes($p['time']);
        if ($runtime > $maxTime) {
          $reason = "Runtime {$runtime}min exceeds {$maxTime}min limit";
        }
      }

      if ($reason !== NULL) {
        $toKill[] = [
          'pid' => $p['pid'],
          'reason' => $reason,
          'memory' => $p['memory_mb'],
          'time' => $p['time'],
        ];
      }
    }

    if (empty($toKill)) {
      $this->io()->success('No stuck processes found within limits.');
      return;
    }

    $this->io()->title(($dryRun ? '[DRY RUN] ' : '') . 'Processes to Kill');

    $rows = [];
    foreach ($toKill as $p) {
      $rows[] = [$p['pid'], $p['memory'] . ' MB', $p['time'], $p['reason']];
    }
    $this->io()->table(['PID', 'Memory', 'Time', 'Reason'], $rows);

    if ($dryRun) {
      $this->io()->note('Dry run - no processes were killed.');
      return;
    }

    if (!$this->io()->confirm('Kill these processes?', FALSE)) {
      $this->logger()?->notice('Aborted.');
      return;
    }

    $killed = 0;
    foreach ($toKill as $p) {
      if (posix_kill((int) $p['pid'], SIGTERM)) {
        $killed++;
      }
    }

    $this->logger()?->success('Killed {count} stuck processes.', ['count' => $killed]);
  }

  /**
   * Gets running FFmpeg processes.
   *
   * @phpstan-return array<int, array{pid: int, cpu: float, memory_percent: float, memory_mb: int, time: string, command: string}>
   */
  protected function getRunningFfmpegProcesses(): array {
    if (PHP_OS_FAMILY === 'Windows') {
      return [];
    }

    $process = new Process(['ps', 'aux']);
    $process->setTimeout(5);
    $process->run();

    if (!$process->isSuccessful()) {
      return [];
    }

    $output = preg_split('/\R/', trim($process->getOutput())) ?: [];

    $processes = [];
    foreach ($output as $line) {
      if (!preg_match('/\bffmpeg\b|\bffprobe\b/', $line)) {
        continue;
      }
      $parts = preg_split('/\s+/', $line, 11);
      if ($parts !== FALSE && count($parts) >= 11) {
        $processes[] = [
          'pid' => (int) $parts[1],
          'cpu' => (float) $parts[2],
          'memory_percent' => (float) $parts[3],
          'memory_mb' => (int) ((int) $parts[5] / 1024),
          'time' => $parts[9] ?? '0:00',
          'command' => $parts[10] ?? '',
        ];
      }
    }

    return $processes;
  }

  /**
   * Parses runtime string to minutes.
   *
   * @param string $time
   *   Time string like "1:23:45" or "23:45".
   *
   * @return int
   *   Runtime in minutes.
   */
  protected function parseRuntimeMinutes(string $time): int {
    $parts = array_reverse(explode(':', $time));
    $seconds = (int) ($parts[0] ?? 0);
    $minutes = (int) ($parts[1] ?? 0);
    $hours = (int) ($parts[2] ?? 0);

    return $hours * 60 + $minutes + (int) ($seconds / 60);
  }

}
