<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Commands;

use Drupal\ffmpeg_media\Service\DeadLetterStorageInterface;
use Drupal\ffmpeg_media\Service\FieldIntegrityServiceInterface;
use Drupal\ffmpeg_media\Service\OutputStorageServiceInterface;
use Drupal\ffmpeg_media\Service\ProgressTrackerInterface;
use Drush\Attributes as CLI;
use Drush\Commands\DrushCommands;

/**
 * Drush commands for FFmpeg Media cleanup and maintenance.
 */
final class CleanupCommands extends DrushCommands {

  /**
   * Constructs a CleanupCommands instance.
   *
   * @param \Drupal\ffmpeg_media\Service\ProgressTrackerInterface $progressTracker
   *   Progress tracker service.
   * @param \Drupal\ffmpeg_media\Service\FieldIntegrityServiceInterface $fieldIntegrity
   *   Field integrity service.
   * @param \Drupal\ffmpeg_media\Service\DeadLetterStorageInterface $deadLetterStorage
   *   Dead-letter storage service.
   * @param \Drupal\ffmpeg_media\Service\OutputStorageServiceInterface $outputStorage
   *   Output storage service.
   */
  public function __construct(
    protected readonly ProgressTrackerInterface $progressTracker,
    protected readonly FieldIntegrityServiceInterface $fieldIntegrity,
    protected readonly DeadLetterStorageInterface $deadLetterStorage,
    protected readonly OutputStorageServiceInterface $outputStorage,
  ) {
    parent::__construct();
  }

  /**
   * Clean up old progress records.
   *
   * @param array $options
   *   Command options.
   *
   * @phpstan-param array{days: int, 'dry-run': bool} $options
   */
  #[CLI\Command(name: 'ffmpeg:cleanup', aliases: ['ffcl'])]
  #[CLI\Option(name: 'days', description: 'Remove records older than N days (default: 7).')]
  #[CLI\Option(name: 'dry-run', description: 'Show what would be cleaned up without actually deleting.')]
  #[CLI\Usage(name: 'ffmpeg:cleanup', description: 'Remove progress records older than 7 days.')]
  #[CLI\Usage(name: 'ffmpeg:cleanup --days=30', description: 'Remove progress records older than 30 days.')]
  #[CLI\Usage(name: 'ffmpeg:cleanup --dry-run', description: 'Show what would be cleaned up.')]
  public function cleanup(array $options = ['days' => 7, 'dry-run' => FALSE]): void {
    $days = (int) $options['days'];
    $maxAge = $days * 86400;
    $dryRun = (bool) $options['dry-run'];

    if ($dryRun) {
      $cutoffDate = date('Y-m-d H:i:s', time() - $maxAge);
      $this->io()->title('[DRY RUN] Cleanup Preview');
      $this->io()->listing([
        "Would remove completed/failed progress records older than {$days} day(s)",
        "Cutoff date: {$cutoffDate}",
      ]);
      $this->io()->note('Dry run - no records were deleted.');
      return;
    }

    $deleted = $this->progressTracker->cleanup($maxAge);

    $this->logger()?->success('Cleaned up {count} old progress records older than {days} days.', [
      'count' => $deleted,
      'days' => $days,
    ]);
  }

  /**
   * Check field definition integrity for ffmpeg_media fields.
   *
   * @param array $options
   *   Command options.
   *
   * @phpstan-param array{repair: bool, 'verify-files': bool} $options
   */
  #[CLI\Command(name: 'ffmpeg:integrity-check', aliases: ['ffic'])]
  #[CLI\Option(name: 'repair', description: 'Automatically repair detected problems.')]
  #[CLI\Option(name: 'verify-files', description: 'Verify that output file references point to existing files on disk.')]
  #[CLI\Usage(name: 'ffmpeg:integrity-check', description: 'Check for field definition integrity issues.')]
  #[CLI\Usage(name: 'ffmpeg:integrity-check --repair', description: 'Check and repair integrity issues.')]
  #[CLI\Usage(name: 'ffmpeg:integrity-check --verify-files', description: 'Check field integrity and verify output file existence.')]
  #[CLI\Usage(name: 'ffmpeg:integrity-check --verify-files --repair', description: 'Check, verify, and repair all issues.')]
  public function integrityCheck(array $options = ['repair' => FALSE, 'verify-files' => FALSE]): void {
    $repair = (bool) $options['repair'];
    $verifyFiles = (bool) $options['verify-files'];

    $this->runFieldIntegrityCheck($repair);

    if ($verifyFiles) {
      $this->runOutputFileVerification($repair);
    }
  }

  /**
   * Runs the field definition integrity check.
   *
   * @param bool $repair
   *   Whether to attempt automatic repair.
   */
  private function runFieldIntegrityCheck(bool $repair): void {
    $this->io()->title('FFmpeg Media Field Integrity Check');

    $problems = $this->fieldIntegrity->check();

    if (empty($problems)) {
      $this->io()->success('No field integrity issues found.');

      $lastCheck = $this->fieldIntegrity->getLastCheckTime();
      if ($lastCheck !== NULL) {
        $this->io()->text('Last check: ' . date('Y-m-d H:i:s', $lastCheck));
      }
      return;
    }

    $this->io()->warning(count($problems) . ' problem(s) found:');

    $rows = [];
    foreach ($problems as $problem) {
      $rows[] = [
        $problem['type'],
        $problem['entity_type'],
        $problem['field_name'],
        $problem['details'],
      ];
    }
    $this->io()->table(['Type', 'Entity Type', 'Field', 'Details'], $rows);

    if (!$repair) {
      $this->io()->note('Run with --repair to fix these issues.');
      return;
    }

    if (!$this->io()->confirm('Repair ' . count($problems) . ' problem(s)?', FALSE)) {
      $this->logger()?->notice('Repair aborted.');
      return;
    }

    $result = $this->fieldIntegrity->repair($problems);

    foreach ($result['details'] as $detail) {
      $this->io()->text('  ' . $detail);
    }

    $this->io()->newLine();

    if ($result['failed'] === 0) {
      $this->io()->success("Repaired {$result['repaired']} problem(s).");
    }
    else {
      $this->io()->warning("Repaired {$result['repaired']}, failed {$result['failed']}.");
    }
  }

  /**
   * Runs output file verification against disk.
   *
   * @param bool $repair
   *   Whether to remove orphaned records.
   */
  private function runOutputFileVerification(bool $repair): void {
    $this->io()->section('Output File Verification');
    $orphaned = $this->outputStorage->verifyOutputFiles();

    if ($orphaned === []) {
      $this->io()->success('All output file references are valid.');
      return;
    }

    $this->io()->warning(count($orphaned) . ' orphaned output record(s) found:');
    $rows = [];
    foreach ($orphaned as $record) {
      $rows[] = [
        $record['media_id'],
        $record['field_name'],
        $record['output_type'],
        $record['file_id'],
        $record['expected_uri'] ?? '-',
        $record['status'],
      ];
    }
    $this->io()->table(
      ['Media ID', 'Field', 'Output Type', 'File ID', 'Expected URI', 'Status'],
      $rows,
    );

    if (!$repair) {
      $this->io()->note('Run with --repair --verify-files to remove orphaned records.');
      return;
    }

    if (!$this->io()->confirm('Remove ' . count($orphaned) . ' orphaned record(s)?', FALSE)) {
      $this->logger()?->notice('Orphaned record removal aborted.');
      return;
    }

    $removed = 0;
    foreach ($orphaned as $record) {
      $this->outputStorage->deleteOrphanedRecord(
        $record['media_id'],
        $record['field_name'],
        $record['output_type'],
        $record['file_id'],
      );
      $removed++;
    }
    $this->io()->success("Removed {$removed} orphaned record(s).");
  }

  /**
   * List permanently failed conversion jobs from the dead-letter queue.
   *
   * @param int|null $mediaId
   *   Optional media ID to inspect a single entry.
   * @param array $options
   *   Command options.
   *
   * @phpstan-param array{limit: int, offset: int, 'error-code': string|null} $options
   */
  #[CLI\Command(name: 'ffmpeg:dead-letters', aliases: ['ffdl'])]
  #[CLI\Argument(name: 'mediaId', description: 'Show details for a specific media ID.')]
  #[CLI\Option(name: 'limit', description: 'Maximum entries to show (default: 50).')]
  #[CLI\Option(name: 'offset', description: 'Skip N entries (default: 0).')]
  #[CLI\Option(name: 'error-code', description: 'Filter by error code (e.g. max_retries_exceeded, ffmpeg_error).')]
  #[CLI\Usage(name: 'ffmpeg:dead-letters', description: 'List all dead-letter entries.')]
  #[CLI\Usage(name: 'ffmpeg:dead-letters 42', description: 'Show details for media ID 42.')]
  #[CLI\Usage(name: 'ffmpeg:dead-letters --error-code=ffmpeg_error', description: 'Show only FFmpeg errors.')]
  #[CLI\Usage(name: 'ffmpeg:dead-letters --limit=10 --offset=20', description: 'Paginated listing.')]
  public function deadLetters(
    ?int $mediaId = NULL,
    array $options = ['limit' => 50, 'offset' => 0, 'error-code' => NULL],
  ): void {
    if ($mediaId !== NULL) {
      $this->showSingleDeadLetter($mediaId);
      return;
    }

    $this->listDeadLetters($options);
  }

  /**
   * Shows details for a single dead-letter entry.
   */
  private function showSingleDeadLetter(int $mediaId): void {
    $entry = $this->deadLetterStorage->getByMediaId($mediaId);

    if ($entry === NULL) {
      $this->io()->warning("No dead-letter entry found for media ID {$mediaId}.");
      return;
    }

    $this->io()->title("Dead-Letter Entry: Media #{$mediaId}");
    $age = (int) floor((time() - $entry['failed_at']) / 86400);
    $this->io()->definitionList(
      ['Media ID' => (string) $entry['media_id']],
      ['Profile' => $entry['profile_id'] ?? '(none)'],
      ['Error Code' => $entry['last_error_code'] ?? '(none)'],
      ['Retries' => (string) $entry['retry_count']],
      ['Failed At' => date('Y-m-d H:i:s', $entry['failed_at'])],
      ['Age' => "{$age} day(s)"],
    );
    $this->io()->section('Error Message');
    $this->io()->text($entry['error_message']);
  }

  /**
   * Lists dead-letter entries with optional filtering.
   *
   * @phpstan-param array{limit: int, offset: int, 'error-code': string|null} $options
   */
  private function listDeadLetters(array $options): void {
    $limit = (int) $options['limit'];
    $offset = (int) $options['offset'];
    $errorCodeFilter = $options['error-code'];
    $total = $this->deadLetterStorage->count();

    if ($total === 0) {
      $this->io()->success('No dead-letter entries found.');
      return;
    }

    $entries = $this->deadLetterStorage->getAll($limit, $offset);

    if ($errorCodeFilter !== NULL) {
      $entries = array_filter(
        $entries,
        static fn(array $entry): bool => $entry['last_error_code'] === $errorCodeFilter,
      );
    }

    $this->io()->title("Dead-Letter Queue ({$total} total)");

    if ($entries === []) {
      $this->io()->note($errorCodeFilter !== NULL
        ? "No entries matching error code '{$errorCodeFilter}'."
        : 'No entries in the requested range.');
      return;
    }

    $rows = [];
    foreach ($entries as $entry) {
      $age = (int) floor((time() - $entry['failed_at']) / 86400);
      $rows[] = [
        $entry['media_id'],
        $entry['last_error_code'] ?? '-',
        $entry['retry_count'],
        date('Y-m-d H:i', $entry['failed_at']),
        "{$age}d",
        mb_substr($entry['error_message'], 0, 60) . (mb_strlen($entry['error_message']) > 60 ? '...' : ''),
      ];
    }

    $this->io()->table(
      ['Media ID', 'Error Code', 'Retries', 'Failed At', 'Age', 'Error Message'],
      $rows,
    );

    if ($total > $offset + $limit) {
      $next = $offset + $limit;
      $this->io()->note("Showing {$offset}-" . ($offset + count($entries)) . " of {$total}. Use --offset={$next} for more.");
    }
  }

}
