<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\ffmpeg_media\Enum\ConversionStatus;

/**
 * Provides cached dashboard statistics for video conversions.
 *
 * @internal
 */
final class DashboardStatsService implements DashboardStatsServiceInterface {

  use StringTranslationTrait;

  /**
   * Cache TTL in seconds (5 minutes).
   */
  private const int CACHE_TTL = 300;

  /**
   * Cache ID prefix for dashboard stats.
   */
  private const string CACHE_PREFIX = 'ffmpeg_media:dashboard_stats';

  /**
   * Progress table name.
   */
  private const string PROGRESS_TABLE = 'ffmpeg_media_progress';

  /**
   * Constructs a DashboardStatsService object.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   The cache backend.
   * @param \Drupal\Component\Datetime\TimeInterface $time
   *   The time service.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   * @param \Drupal\Core\Datetime\DateFormatterInterface $dateFormatter
   *   The date formatter.
   * @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cacheTagsInvalidator
   *   The cache tags invalidator.
   * @param \Drupal\Core\Logger\LoggerChannelInterface $logger
   *   The logger channel.
   */
  public function __construct(
    protected readonly Connection $database,
    protected readonly CacheBackendInterface $cache,
    protected readonly TimeInterface $time,
    protected readonly EntityTypeManagerInterface $entityTypeManager,
    protected readonly DateFormatterInterface $dateFormatter,
    protected readonly CacheTagsInvalidatorInterface $cacheTagsInvalidator,
    protected readonly LoggerChannelInterface $logger,
  ) {}

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getConversionStats(): array {
    $cid = self::CACHE_PREFIX . ':conversion_stats';
    $cached = $this->cache->get($cid);

    if ($cached !== FALSE && is_array($cached->data)) {
      /** @var array{completed: int, failed: int, pending: int, converting: int, total: int, success_rate: float} $data */
      $data = $cached->data;
      return $data;
    }

    $stats = $this->calculateConversionStats();

    $this->cache->set(
      $cid,
      $stats,
      $this->time->getRequestTime() + self::CACHE_TTL,
      ['ffmpeg_media:stats'],
    );

    return $stats;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getRecentConversions(int $limit = 20): array {
    $cid = self::CACHE_PREFIX . ':recent_conversions:' . $limit;
    $cached = $this->cache->get($cid);

    if ($cached !== FALSE && is_array($cached->data)) {
      return $cached->data;
    }

    $conversions = $this->fetchRecentConversions($limit);

    $this->cache->set(
      $cid,
      $conversions,
      $this->time->getRequestTime() + self::CACHE_TTL,
      ['ffmpeg_media:stats'],
    );

    return $conversions;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getAverageConversionTime(): ?float {
    $cid = self::CACHE_PREFIX . ':average_time';
    $cached = $this->cache->get($cid);

    if ($cached !== FALSE) {
      return $cached->data;
    }

    $averageTime = $this->calculateAverageConversionTime();

    $this->cache->set(
      $cid,
      $averageTime,
      $this->time->getRequestTime() + self::CACHE_TTL,
      ['ffmpeg_media:stats'],
    );

    return $averageTime;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getSuccessRate(): float {
    $stats = $this->get24HourStats();
    return $stats['success_rate'];
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function getRecentFailures(int $limit = 10): array {
    $cid = self::CACHE_PREFIX . ':recent_failures:' . $limit;
    $cached = $this->cache->get($cid);

    if ($cached !== FALSE && is_array($cached->data)) {
      return $cached->data;
    }

    $failures = $this->fetchRecentFailures($limit);

    $this->cache->set(
      $cid,
      $failures,
      $this->time->getRequestTime() + self::CACHE_TTL,
      ['ffmpeg_media:stats'],
    );

    return $failures;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  #[\NoDiscard]
  public function get24HourStats(): array {
    $cid = self::CACHE_PREFIX . ':24h_stats';
    $cached = $this->cache->get($cid);

    if ($cached !== FALSE && is_array($cached->data)) {
      /** @var array{completed: int, failed: int, total: int, success_rate: float, average_time: float|null} $data */
      $data = $cached->data;
      return $data;
    }

    $stats = $this->calculate24HourStats();

    $this->cache->set(
      $cid,
      $stats,
      $this->time->getRequestTime() + self::CACHE_TTL,
      ['ffmpeg_media:stats'],
    );

    return $stats;
  }

  /**
   * {@inheritdoc}
   */
  #[\Override]
  public function clearCache(): void {
    $this->cacheTagsInvalidator->invalidateTags(['ffmpeg_media:stats']);
  }

  /**
   * Calculates conversion statistics.
   *
   * @return array{completed: int, failed: int, pending: int, converting: int, total: int, success_rate: float}
   *   Conversion statistics.
   */
  protected function calculateConversionStats(): array {
    try {
      // Count all statuses in a single GROUP BY query.
      $query = $this->database->select(self::PROGRESS_TABLE, 'p')
        ->fields('p', ['status'])
        ->groupBy('status');
      $query->addExpression('COUNT(*)', 'count');
      $results = $query->execute()?->fetchAll() ?? [];

      $counts = [
        ConversionStatus::Completed->value => 0,
        ConversionStatus::Failed->value => 0,
        ConversionStatus::Pending->value => 0,
        ConversionStatus::Converting->value => 0,
      ];

      foreach ($results as $row) {
        if (isset($counts[$row->status])) {
          $counts[$row->status] = (int) $row->count;
        }
      }

      $completed = $counts[ConversionStatus::Completed->value];
      $failed = $counts[ConversionStatus::Failed->value];
      $pending = $counts[ConversionStatus::Pending->value];
      $converting = $counts[ConversionStatus::Converting->value];

      $total = $completed + $failed + $pending + $converting;
      $finishedTotal = $completed + $failed;
      $successRate = $finishedTotal > 0
        ? round(($completed / $finishedTotal) * 100, 1)
        : 100.0;

      return [
        'completed' => $completed,
        'failed' => $failed,
        'pending' => $pending,
        'converting' => $converting,
        'total' => $total,
        'success_rate' => $successRate,
      ];
    }
    catch (\Throwable) {
      return [
        'completed' => 0,
        'failed' => 0,
        'pending' => 0,
        'converting' => 0,
        'total' => 0,
        'success_rate' => 0.0,
      ];
    }
  }

  /**
   * Fetches recent conversions from the database.
   *
   * @param int $limit
   *   Maximum number of conversions to fetch.
   *
   * @phpstan-return array<array{
   *   media_id: int,
   *   media_name: string,
   *   media_url: string|null,
   *   status: string,
   *   started_at: int|null,
   *   completed_at: int|null,
   *   completed_at_formatted: string,
   *   duration: string,
   *   error_message: string|null,
   * }>
   *   Array of recent conversion data.
   */
  protected function fetchRecentConversions(int $limit): array {
    $conversions = [];

    try {
      $query = $this->database->select(self::PROGRESS_TABLE, 'p')
        ->fields('p', [
          'media_id',
          'status',
          'started',
          'completed',
          'error_message',
        ])
        ->condition('status', [
          ConversionStatus::Completed->value,
          ConversionStatus::Failed->value,
        ], 'IN')
        ->orderBy('completed', 'DESC')
        ->range(0, $limit);

      $statement = $query->execute();
      $results = $statement?->fetchAll() ?? [];

      if (empty($results)) {
        return $conversions;
      }

      // Batch load media entities.
      $mediaIds = array_column($results, 'media_id');
      $mediaEntities = $this->loadMediaEntities($mediaIds);

      foreach ($results as $row) {
        $mediaId = (int) $row->media_id;
        $media = $mediaEntities[$mediaId] ?? NULL;

        $duration = '';
        if (!empty($row->started) && !empty($row->completed)) {
          $elapsed = (int) $row->completed - (int) $row->started;
          $duration = $this->dateFormatter->formatInterval($elapsed);
        }

        $conversions[] = [
          'media_id' => $mediaId,
          'media_name' => $media?->getName() ?? (string) $this->t('Unknown'),
          'media_url' => $this->getMediaEditUrl($media),
          'status' => $row->status,
          'started_at' => $row->started ? (int) $row->started : NULL,
          'completed_at' => $row->completed ? (int) $row->completed : NULL,
          'completed_at_formatted' => $row->completed
            ? $this->dateFormatter->format((int) $row->completed, 'short')
            : '',
          'duration' => $duration,
          'error_message' => $row->error_message,
        ];
      }
    }
    catch (\Throwable) {
      // Return empty array on database error.
    }

    return $conversions;
  }

  /**
   * Calculates average conversion time for last 24 hours.
   *
   * @return float|null
   *   Average time in seconds or NULL if no data.
   */
  protected function calculateAverageConversionTime(): ?float {
    try {
      $threshold = $this->time->getRequestTime() - (24 * 60 * 60);

      $query = $this->database->select(self::PROGRESS_TABLE, 'p')
        ->condition('status', ConversionStatus::Completed->value)
        ->condition('completed', $threshold, '>')
        ->isNotNull('started')
        ->isNotNull('completed');

      $query->addExpression('AVG([completed] - [started])', 'avg_time');

      $statement = $query->execute();
      $result = $statement?->fetchField();

      return $result !== FALSE && $result !== NULL
        ? round((float) $result, 1)
        : NULL;
    }
    catch (\Throwable) {
      return NULL;
    }
  }

  /**
   * Fetches recent failures from the database.
   *
   * @param int $limit
   *   Maximum number of failures to fetch.
   *
   * @phpstan-return array<array{
   *   media_id: int,
   *   media_name: string,
   *   media_url: string|null,
   *   error_message: string,
   *   failed_at: int,
   *   failed_at_formatted: string,
   * }>
   *   Array of recent failure data.
   */
  protected function fetchRecentFailures(int $limit): array {
    $failures = [];

    try {
      $query = $this->database->select(self::PROGRESS_TABLE, 'p')
        ->fields('p', ['media_id', 'error_message', 'completed'])
        ->condition('status', ConversionStatus::Failed->value)
        ->orderBy('completed', 'DESC')
        ->range(0, $limit);

      $statement = $query->execute();
      $results = $statement?->fetchAll() ?? [];

      if (empty($results)) {
        return $failures;
      }

      // Batch load media entities.
      $mediaIds = array_column($results, 'media_id');
      $mediaEntities = $this->loadMediaEntities($mediaIds);

      foreach ($results as $row) {
        $mediaId = (int) $row->media_id;
        $media = $mediaEntities[$mediaId] ?? NULL;

        $failures[] = [
          'media_id' => $mediaId,
          'media_name' => $media?->getName() ?? (string) $this->t('Unknown'),
          'media_url' => $this->getMediaEditUrl($media),
          'error_message' => $row->error_message ?? (string) $this->t('Unknown error'),
          'failed_at' => $row->completed ? (int) $row->completed : 0,
          'failed_at_formatted' => $row->completed
            ? $this->dateFormatter->format((int) $row->completed, 'short')
            : '',
        ];
      }
    }
    catch (\Throwable) {
      // Return empty array on database error.
    }

    return $failures;
  }

  /**
   * Calculates 24-hour statistics.
   *
   * @return array{completed: int, failed: int, total: int, success_rate: float, average_time: float|null}
   *   24-hour statistics.
   */
  protected function calculate24HourStats(): array {
    try {
      $threshold = $this->time->getRequestTime() - (24 * 60 * 60);

      // Single query: counts + average time in one DB round-trip.
      $query = $this->database->select(self::PROGRESS_TABLE, 'p')
        ->condition('status', [
          ConversionStatus::Completed->value,
          ConversionStatus::Failed->value,
        ], 'IN')
        ->condition('completed', $threshold, '>');

      $query->addExpression(
        "SUM(CASE WHEN [status] = :completed THEN 1 ELSE 0 END)",
        'completed_count',
        [':completed' => ConversionStatus::Completed->value],
      );
      $query->addExpression(
        "SUM(CASE WHEN [status] = :failed THEN 1 ELSE 0 END)",
        'failed_count',
        [':failed' => ConversionStatus::Failed->value],
      );
      $query->addExpression(
        "AVG(CASE WHEN [status] = :avg_status AND [started] IS NOT NULL AND [completed] IS NOT NULL THEN [completed] - [started] ELSE NULL END)",
        'avg_time',
        [':avg_status' => ConversionStatus::Completed->value],
      );

      $row = $query->execute()?->fetchObject();

      $completed = ($row !== FALSE && $row !== NULL) ? (int) $row->completed_count : 0;
      $failed = ($row !== FALSE && $row !== NULL) ? (int) $row->failed_count : 0;
      $total = $completed + $failed;
      $successRate = $total > 0
        ? round(($completed / $total) * 100, 1)
        : 100.0;
      $averageTime = ($row !== FALSE && $row !== NULL && $row->avg_time !== NULL)
        ? round((float) $row->avg_time, 1)
        : NULL;

      return [
        'completed' => $completed,
        'failed' => $failed,
        'total' => $total,
        'success_rate' => $successRate,
        'average_time' => $averageTime,
      ];
    }
    catch (\Throwable) {
      return [
        'completed' => 0,
        'failed' => 0,
        'total' => 0,
        'success_rate' => 0.0,
        'average_time' => NULL,
      ];
    }
  }

  /**
   * Loads media entities by IDs.
   *
   * @param array<int|string> $mediaIds
   *   Array of media entity IDs.
   *
   * @return array<int|string, \Drupal\media\MediaInterface>
   *   Array of media entities keyed by ID.
   */
  protected function loadMediaEntities(array $mediaIds): array {
    if (empty($mediaIds)) {
      return [];
    }

    try {
      return $this->entityTypeManager
        ->getStorage('media')
        ->loadMultiple($mediaIds);
    }
    catch (\Throwable $e) {
      $this->logger->warning('Failed to load media entities: @error', [
        '@error' => $e->getMessage(),
      ]);
      return [];
    }
  }

  /**
   * Gets the edit URL for a media entity.
   *
   * @param mixed $media
   *   The media entity or NULL.
   *
   * @return string|null
   *   The edit URL or NULL.
   */
  protected function getMediaEditUrl(mixed $media): ?string {
    if ($media === NULL) {
      return NULL;
    }

    try {
      return $media->toUrl('edit-form')->toString();
    }
    catch (\Throwable $e) {
      $this->logger->warning('Failed to get media edit URL: @error', [
        '@error' => $e->getMessage(),
      ]);
      return NULL;
    }
  }

}
