<?php

declare(strict_types=1);

namespace Drupal\ffmpeg_media\Service;

/**
 * Formats collected FFmpeg Media metrics for Prometheus exposition.
 *
 * @internal
 */
final class MetricsPrometheusFormatter {

  /**
   * Builds the Prometheus exposition payload.
   *
   * @param array<string, mixed> $conversion
   *   Conversion metrics.
   * @param array<string, mixed> $queue
   *   Queue metrics.
   * @param array<string, mixed> $hardware
   *   Hardware metrics.
   * @param array<string, mixed> $system
   *   System metrics.
   * @param array<string, string> $labels
   *   Global labels.
   *
   * @return string
   *   Prometheus exposition payload.
   */
  #[\NoDiscard]
  public function format(array $conversion, array $queue, array $hardware, array $system, array $labels = []): string {
    $output = [];
    $labelString = $this->buildLabelString($labels);

    $output[] = $this->formatMetric(
      'ffmpeg_media_conversions_total',
      'counter',
      'Total number of video conversions',
      $conversion['total'],
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_conversions_successful_total',
      'counter',
      'Total number of successful conversions',
      $conversion['successful'],
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_conversions_failed_total',
      'counter',
      'Total number of failed conversions',
      $conversion['failed'],
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_conversions_in_progress',
      'gauge',
      'Number of conversions currently in progress',
      $conversion['in_progress'],
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_conversion_duration_seconds_avg',
      'gauge',
      'Average conversion duration in seconds',
      $conversion['avg_duration_seconds'],
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_conversion_duration_seconds_total',
      'counter',
      'Total conversion time in seconds',
      $conversion['total_duration_seconds'],
      $labelString,
    );

    foreach ($conversion['by_codec'] as $codec => $stats) {
      $codecLabels = $this->appendLabel($labelString, 'codec', (string) $codec);
      $output[] = $this->formatMetricValue(
        'ffmpeg_media_conversions_by_codec_total',
        $stats['total'],
        $codecLabels,
      );
    }

    foreach ($conversion['by_output'] as $format => $stats) {
      $formatLabels = $this->appendLabel($labelString, 'format', (string) $format);
      $output[] = $this->formatMetricValue(
        'ffmpeg_media_conversions_by_format_total',
        $stats['total'],
        $formatLabels,
      );
    }

    $output[] = $this->formatMetric(
      'ffmpeg_media_queue_size',
      'gauge',
      'Current queue size',
      $queue['total_queue_size'],
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_queue_standard_size',
      'gauge',
      'Standard queue size',
      $queue['standard_queue_size'],
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_queue_large_size',
      'gauge',
      'Large file queue size',
      $queue['large_queue_size'],
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_queue_wait_seconds_avg',
      'gauge',
      'Average queue wait time in seconds',
      $queue['avg_wait_time_seconds'],
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_queue_wait_seconds_max',
      'gauge',
      'Maximum queue wait time in seconds',
      $queue['max_wait_time_seconds'],
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_queue_processed_total',
      'counter',
      'Total items processed from queue',
      $queue['items_processed_total'],
      $labelString,
    );

    $output[] = $this->formatMetric(
      'ffmpeg_media_hardware_available',
      'gauge',
      'Whether hardware acceleration is available (1=yes, 0=no)',
      $hardware['hardware_available'] ? 1 : 0,
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_hardware_enabled',
      'gauge',
      'Whether hardware acceleration is enabled (1=yes, 0=no)',
      $hardware['hardware_enabled'] ? 1 : 0,
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_circuit_breaker_open',
      'gauge',
      'Whether circuit breaker is open (1=open, 0=closed)',
      $hardware['circuit_breaker_open'] ? 1 : 0,
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_hw_conversions_total',
      'counter',
      'Total hardware-accelerated conversions',
      $hardware['hw_conversions_total'],
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_sw_fallback_total',
      'counter',
      'Total software fallback events',
      $hardware['sw_fallback_total'],
      $labelString,
    );

    $output[] = $this->formatMetric(
      'ffmpeg_media_concurrent_conversions',
      'gauge',
      'Number of concurrent conversions',
      $system['concurrent_conversions'],
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_max_concurrent',
      'gauge',
      'Maximum allowed concurrent conversions',
      $system['max_concurrent'],
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_locks_held',
      'gauge',
      'Number of conversion locks currently held',
      $system['locks_held'],
      $labelString,
    );
    $output[] = $this->formatMetric(
      'ffmpeg_media_ffmpeg_processes',
      'gauge',
      'Number of running FFmpeg processes',
      $system['ffmpeg_processes'],
      $labelString,
    );

    return $output
      |> array_filter(...)
      |> (fn(array $lines): string => implode("\n", $lines))
      |> (fn(string $lines): string => $lines . "\n");
  }

  /**
   * Formats a metric with HELP and TYPE lines.
   */
  #[\NoDiscard]
  private function formatMetric(
    string $name,
    string $type,
    string $help,
    int|float $value,
    string $labels = '',
  ): string {
    $output = "# HELP {$name} {$help}\n";
    $output .= "# TYPE {$name} {$type}\n";
    $output .= $this->formatMetricValue($name, $value, $labels);

    return $output;
  }

  /**
   * Formats a metric value line.
   */
  #[\NoDiscard]
  private function formatMetricValue(string $name, int|float $value, string $labels = ''): string {
    if ($labels !== '') {
      return "{$name}{{$labels}} {$value}";
    }

    return "{$name} {$value}";
  }

  /**
   * Appends a single label to a Prometheus label string.
   */
  #[\NoDiscard]
  private function appendLabel(string $baseLabels, string $key, string $value): string {
    $label = $key . '="' . $this->escapeLabelValue($value) . '"';
    if ($baseLabels === '') {
      return $label;
    }

    return $baseLabels . ',' . $label;
  }

  /**
   * Escapes a Prometheus label value.
   */
  #[\NoDiscard]
  private function escapeLabelValue(string $value): string {
    return str_replace(
      ['\\', '"', "\n", "\r"],
      ['\\\\', '\\"', '\\n', '\\r'],
      $value,
    );
  }

  /**
   * Builds a serialized label string from a label map.
   *
   * @param array<string, string> $labels
   *   Labels keyed by name.
   */
  #[\NoDiscard]
  private function buildLabelString(array $labels): string {
    if ($labels === []) {
      return '';
    }

    $parts = [];
    foreach ($labels as $key => $value) {
      $parts[] = $key . '="' . $this->escapeLabelValue($value) . '"';
    }

    return implode(',', $parts);
  }

}
