<?php

declare(strict_types=1);

namespace Drupal\native_observability_otel\Service;

use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Statement\FetchAs;

/**
 * Reads live observability metrics from source tables for Prometheus scraping.
 */
final class LivePrometheusMetricsReader {

  private const TRACE_TABLE = 'native_observability_trace';
  private const SPANS_TABLE = 'native_observability_spans';
  private const CACHE_TABLE = 'native_observability_cache_event';

  /**
   * Constructs the live metrics reader.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   The active database connection.
   */
  public function __construct(
    private readonly Connection $database,
  ) {
  }

  /**
   * Builds HTTP metrics in Prometheus text exposition format.
   *
   * @param int $windowSeconds
   *   Rolling window size in seconds.
   *
   * @return string
   *   Prometheus text exposition output.
   */
  public function buildHttpMetrics(int $windowSeconds): string {
    $cutoff = \time() - $windowSeconds;
    $lines = [];

    $lines = array_merge($lines, $this->buildHttpRequestsTotalMetric());
    $lines[] = '';
    $lines = array_merge($lines, $this->buildHttpRequestsWindowMetric($cutoff, $windowSeconds));
    $lines[] = '';
    $lines = array_merge($lines, $this->buildHttpDurationPercentilesMetric($cutoff));
    $lines[] = '';
    $lines = array_merge($lines, $this->buildHttpErrorRateMetric($cutoff));
    $lines[] = '';
    $lines = array_merge($lines, $this->buildHttpAjaxRequestsTotalMetric());

    return implode("\n", $lines) . "\n";
  }

  /**
   * Builds cache metrics in Prometheus text exposition format.
   *
   * Returns an empty string when the cache observer table does not exist.
   *
   * @return string
   *   Prometheus text exposition output.
   */
  public function buildCacheMetrics(): string {
    if (!$this->database->schema()->tableExists(self::CACHE_TABLE)) {
      return '';
    }

    $lines = [
      '# HELP drupal_cache_operations_total Total cache events by operation and bin (all time).',
      '# TYPE drupal_cache_operations_total counter',
    ];

    $query = $this->database->select(self::CACHE_TABLE, 'c');
    $query->fields('c', ['status', 'cache_layer']);
    $query->addExpression('COUNT(*)', 'cnt');
    $query->isNotNull('c.status');
    $query->isNotNull('c.cache_layer');
    $query->groupBy('c.status');
    $query->groupBy('c.cache_layer');

    $rows = $query->execute()->fetchAll(FetchAs::Associative) ?: [];

    foreach ($rows as $row) {
      $lines[] = sprintf(
        'drupal_cache_operations_total{operation="%s",bin="%s"} %d',
        $this->sanitizeLabelValue((string) ($row['status'] ?? 'unknown')),
        $this->sanitizeLabelValue((string) ($row['cache_layer'] ?? 'unknown')),
        (int) ($row['cnt'] ?? 0),
      );
    }

    return implode("\n", $lines) . "\n";
  }

  /**
   * Builds span metrics in Prometheus text exposition format.
   *
   * Returns an empty string when the spans table does not exist.
   *
   * @param int $windowSeconds
   *   Rolling window size in seconds.
   *
   * @return string
   *   Prometheus text exposition output.
   */
  public function buildSpanMetrics(int $windowSeconds): string {
    if (!$this->database->schema()->tableExists(self::SPANS_TABLE)) {
      return '';
    }

    $cutoff_ms = (\time() - $windowSeconds) * 1000;
    $lines = [];

    $query = $this->database->select(self::SPANS_TABLE, 's');
    $query->fields('s', ['category']);
    $query->addExpression('AVG(s.duration_ms)', 'avg_duration');
    $query->addExpression('COUNT(*)', 'cnt');
    $query->condition('s.started_at_ms', $cutoff_ms, '>=');
    $query->isNotNull('s.category');
    $query->isNotNull('s.duration_ms');
    $query->groupBy('s.category');

    $rows = $query->execute()->fetchAll(FetchAs::Associative) ?: [];

    $lines[] = '# HELP drupal_span_duration_avg_ms Average span duration in milliseconds by category in rolling window.';
    $lines[] = '# TYPE drupal_span_duration_avg_ms gauge';
    foreach ($rows as $row) {
      $lines[] = sprintf(
        'drupal_span_duration_avg_ms{category="%s"} %.3f',
        $this->sanitizeLabelValue((string) ($row['category'] ?? 'unknown')),
        (float) ($row['avg_duration'] ?? 0),
      );
    }
    $lines[] = '';

    $lines[] = '# HELP drupal_span_count Number of spans by category in rolling window.';
    $lines[] = '# TYPE drupal_span_count gauge';
    foreach ($rows as $row) {
      $lines[] = sprintf(
        'drupal_span_count{category="%s"} %d',
        $this->sanitizeLabelValue((string) ($row['category'] ?? 'unknown')),
        (int) ($row['cnt'] ?? 0),
      );
    }

    return implode("\n", $lines) . "\n";
  }

  /**
   * Builds the drupal_http_requests_total counter (all-time).
   *
   * @return string[]
   *   Metric lines.
   */
  private function buildHttpRequestsTotalMetric(): array {
    $lines = [
      '# HELP drupal_http_requests_total Total HTTP requests recorded (all time).',
      '# TYPE drupal_http_requests_total counter',
    ];

    $query = $this->database->select(self::TRACE_TABLE, 't');
    $query->fields('t', ['route_name', 'path', 'method', 'status_code']);
    $query->addExpression('COUNT(*)', 'cnt');
    $query->groupBy('t.route_name');
    $query->groupBy('t.path');
    $query->groupBy('t.method');
    $query->groupBy('t.status_code');

    $rows = $query->execute()->fetchAll(FetchAs::Associative) ?: [];

    $series = [];
    foreach ($rows as $row) {
      $route = $this->routeLabel($row);
      $method = $this->sanitizeLabelValue(strtoupper((string) ($row['method'] ?? 'unknown')));
      $status_code = $this->sanitizeLabelValue((string) ($row['status_code'] ?? 'unknown'));
      $key = implode("\0", [$route, $method, $status_code]);

      if (!isset($series[$key])) {
        $series[$key] = [
          'route' => $route,
          'method' => $method,
          'status_code' => $status_code,
          'cnt' => 0,
        ];
      }
      $series[$key]['cnt'] += (int) ($row['cnt'] ?? 0);
    }

    foreach ($series as $item) {
      $lines[] = sprintf(
        'drupal_http_requests_total{route="%s",method="%s",status_code="%s"} %d',
        $item['route'],
        $item['method'],
        $item['status_code'],
        $item['cnt'],
      );
    }

    return $lines;
  }

  /**
   * Builds the drupal_http_requests_window gauge (rolling window).
   *
   * @param int $cutoff
   *   Unix timestamp start of rolling window.
   * @param int $windowSeconds
   *   Window duration label value.
   *
   * @return string[]
   *   Metric lines.
   */
  private function buildHttpRequestsWindowMetric(int $cutoff, int $windowSeconds): array {
    $lines = [
      sprintf('# HELP drupal_http_requests_window HTTP requests in the last %d seconds.', $windowSeconds),
      '# TYPE drupal_http_requests_window gauge',
    ];

    $query = $this->database->select(self::TRACE_TABLE, 't');
    $query->fields('t', ['route_name', 'path', 'method', 'status_code']);
    $query->addExpression('COUNT(*)', 'cnt');
    $query->condition('t.started_at', $cutoff, '>=');
    $query->groupBy('t.route_name');
    $query->groupBy('t.path');
    $query->groupBy('t.method');
    $query->groupBy('t.status_code');

    $rows = $query->execute()->fetchAll(FetchAs::Associative) ?: [];

    $series = [];
    foreach ($rows as $row) {
      $route = $this->routeLabel($row);
      $method = $this->sanitizeLabelValue(strtoupper((string) ($row['method'] ?? 'unknown')));
      $status_code = $this->sanitizeLabelValue((string) ($row['status_code'] ?? 'unknown'));
      $key = implode("\0", [$route, $method, $status_code, $windowSeconds]);

      if (!isset($series[$key])) {
        $series[$key] = [
          'route' => $route,
          'method' => $method,
          'status_code' => $status_code,
          'cnt' => 0,
        ];
      }
      $series[$key]['cnt'] += (int) ($row['cnt'] ?? 0);
    }

    foreach ($series as $item) {
      $lines[] = sprintf(
        'drupal_http_requests_window{route="%s",method="%s",status_code="%s",window_seconds="%d"} %d',
        $item['route'],
        $item['method'],
        $item['status_code'],
        $windowSeconds,
        $item['cnt'],
      );
    }

    return $lines;
  }

  /**
   * Builds the drupal_http_request_duration_ms summary (P50/P95/P99 per route).
   *
   * @param int $cutoff
   *   Unix timestamp start of rolling window.
   *
   * @return string[]
   *   Metric lines.
   */
  private function buildHttpDurationPercentilesMetric(int $cutoff): array {
    $lines = [
      '# HELP drupal_http_request_duration_ms Request duration in milliseconds by quantile and route.',
      '# TYPE drupal_http_request_duration_ms summary',
    ];

    $query = $this->database->select(self::TRACE_TABLE, 't');
    $query->fields('t', ['route_name', 'path', 'duration_ms']);
    $query->condition('t.started_at', $cutoff, '>=');
    $query->isNotNull('t.duration_ms');
    $query->orderBy('t.route_name');
    $query->orderBy('t.path');
    $query->orderBy('t.duration_ms', 'ASC');

    $rows = $query->execute()->fetchAll(FetchAs::Associative) ?: [];

    $byRoute = [];
    foreach ($rows as $row) {
      $key = $this->routeLabel($row);
      $byRoute[$key][] = (float) ($row['duration_ms'] ?? 0);
    }

    foreach ($byRoute as $routeKey => $durations) {
      foreach ([0.5, 0.95, 0.99] as $quantile) {
        $lines[] = sprintf(
          'drupal_http_request_duration_ms{route="%s",quantile="%.2f"} %.3f',
          $routeKey,
          $quantile,
          $this->computePercentile($durations, $quantile),
        );
      }
    }

    return $lines;
  }

  /**
   * Builds the drupal_http_error_rate gauge (% 5xx in rolling window).
   *
   * @param int $cutoff
   *   Unix timestamp start of rolling window.
   *
   * @return string[]
   *   Metric lines.
   */
  private function buildHttpErrorRateMetric(int $cutoff): array {
    $lines = [
      '# HELP drupal_http_error_rate Percentage of 5xx responses in rolling window.',
      '# TYPE drupal_http_error_rate gauge',
    ];

    $query = $this->database->select(self::TRACE_TABLE, 't');
    $query->fields('t', ['route_name', 'path']);
    $query->addExpression('SUM(CASE WHEN t.status_code >= 500 THEN 1 ELSE 0 END)', 'errors');
    $query->addExpression('COUNT(*)', 'total');
    $query->condition('t.started_at', $cutoff, '>=');
    $query->groupBy('t.route_name');
    $query->groupBy('t.path');

    $rows = $query->execute()->fetchAll(FetchAs::Associative) ?: [];

    $series = [];
    foreach ($rows as $row) {
      $route = $this->routeLabel($row);
      if (!isset($series[$route])) {
        $series[$route] = [
          'errors' => 0,
          'total' => 0,
        ];
      }
      $series[$route]['errors'] += (int) ($row['errors'] ?? 0);
      $series[$route]['total'] += (int) ($row['total'] ?? 0);
    }

    foreach ($series as $route => $item) {
      $total = (int) $item['total'];
      $errors = (int) $item['errors'];
      $rate = $total > 0 ? round($errors / $total * 100, 2) : 0.0;

      $lines[] = sprintf(
        'drupal_http_error_rate{route="%s"} %.2f',
        $route,
        $rate,
      );
    }

    return $lines;
  }

  /**
   * Builds the drupal_http_ajax_requests_total counter (all-time AJAX).
   *
   * @return string[]
   *   Metric lines.
   */
  private function buildHttpAjaxRequestsTotalMetric(): array {
    $lines = [
      '# HELP drupal_http_ajax_requests_total Total AJAX HTTP requests recorded (all time).',
      '# TYPE drupal_http_ajax_requests_total counter',
    ];

    $query = $this->database->select(self::TRACE_TABLE, 't');
    $query->fields('t', ['route_name', 'path']);
    $query->addExpression('COUNT(*)', 'cnt');
    $query->condition('t.is_ajax', 1);
    $query->groupBy('t.route_name');
    $query->groupBy('t.path');

    $rows = $query->execute()->fetchAll(FetchAs::Associative) ?: [];

    $series = [];
    foreach ($rows as $row) {
      $route = $this->routeLabel($row);
      $series[$route] = ($series[$route] ?? 0) + (int) ($row['cnt'] ?? 0);
    }

    foreach ($series as $route => $count) {
      $lines[] = sprintf(
        'drupal_http_ajax_requests_total{route="%s"} %d',
        $route,
        $count,
      );
    }

    return $lines;
  }

  /**
   * Computes the value at the given quantile from a sorted array.
   *
   * @param float[] $sortedValues
   *   Values sorted in ascending order.
   * @param float $quantile
   *   Quantile between 0 and 1.
   *
   * @return float
   *   The computed percentile value.
   */
  private function computePercentile(array $sortedValues, float $quantile): float {
    $count = count($sortedValues);
    if ($count === 0) {
      return 0.0;
    }
    $index = (int) ceil($count * $quantile) - 1;
    $index = max(0, min($index, $count - 1));
    return round($sortedValues[$index], 3);
  }

  /**
   * Resolves the route label from a trace row.
   *
   * Uses route_name when available, falls back to path.
   *
   * @param array $row
   *   A trace result row.
   *
   * @return string
   *   The sanitized route label.
   */
  private function routeLabel(array $row): string {
    $label = (string) ($row['route_name'] ?? '');
    if ($label === '') {
      $label = (string) ($row['path'] ?? 'unknown');
    }
    return $this->sanitizeLabelValue($label);
  }

  /**
   * Sanitizes a string for safe use as a Prometheus label value.
   *
   * @param string $value
   *   The raw value.
   *
   * @return string
   *   The sanitized value.
   */
  private function sanitizeLabelValue(string $value): string {
    return addcslashes($value, "\\\"\n\r\t");
  }

}
