<?php

declare(strict_types=1);

namespace Drupal\native_observability_dashboard\Service;

use Drupal\Core\Database\Connection;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\native_observability_dashboard\ValueObject\TimeRange;

/**
 * Builds derived cache observability metrics for dashboard consumption.
 *
 * This service intentionally avoids introducing new storage layers:
 * - Request-level cache metadata is read from synthetic HTTP request spans.
 * - Cross-request invalidation events are read from cache observer events.
 * - Aggregation is performed in PHP on a bounded result set to keep the
 *   implementation lightweight and aligned with the current architecture.
 */
final class CacheObservabilityMetricsBuilder {

  use StringTranslationTrait;

  /**
   * The cache observer events table.
   */
  private const CACHE_EVENT_TABLE = 'native_observability_cache_event';

  /**
   * The spans table.
   */
  private const SPANS_TABLE = 'native_observability_spans';

  /**
   * Max synthetic request spans processed per dashboard range.
   *
   * This defensive upper bound limits JSON decoding overhead in development and
   * keeps the dashboard aggregation lightweight.
   */
  private const MAX_REQUEST_SUMMARY_SPANS = 1500;

  /**
   * Creates a new cache observability metrics builder instance.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection.
   */
  public function __construct(
    private readonly Connection $database,
  ) {
  }

  /**
   * Builds the derived cache observability metrics for the selected range.
   *
   * @param \Drupal\native_observability_dashboard\ValueObject\TimeRange $range
   *   The selected dashboard time range.
   *
   * @return array
   *   A structured metrics payload ready for rendering.
   */
  public function build(TimeRange $range): array {
    $metrics = $this->buildEmptyMetrics();

    $this->aggregateRequestSummarySpans($metrics, $range);
    $this->aggregateInvalidationEvents($metrics, $range);

    $metrics['summary']['total_events'] =
      $metrics['summary']['response_cache_events']
      + $metrics['summary']['cacheability_events']
      + $metrics['summary']['invalidation_events'];

    $metrics['signals']['http_cache_activity_detected'] =
      $metrics['behavior']['page_cache']['total'] > 0
      || $metrics['behavior']['dynamic_page_cache']['total'] > 0;

    $this->finalizeBehaviorMetrics($metrics['behavior']['page_cache']);
    $this->finalizeBehaviorMetrics($metrics['behavior']['dynamic_page_cache']);

    $metrics['behavior']['overall'] = $this->buildOverallBehaviorMetrics(
      $metrics['behavior']['page_cache'],
      $metrics['behavior']['dynamic_page_cache'],
    );

    $metrics['cacheability']['ratios']['cacheable_ratio'] = $this->calculateRatio(
      $metrics['cacheability']['cacheable_count'],
      $metrics['cacheability']['total_checks'],
    );

    $metrics['cacheability']['ratios']['uncacheable_ratio'] = $this->calculateRatio(
      $metrics['cacheability']['uncacheable_count'],
      $metrics['cacheability']['total_checks'],
    );

    $metrics['cacheability']['averages'] = [
      'avg_tags' => $this->calculateAverage(
        $metrics['cacheability']['totals']['tags'],
        $metrics['cacheability']['total_checks'],
      ),
      'avg_contexts' => $this->calculateAverage(
        $metrics['cacheability']['totals']['contexts'],
        $metrics['cacheability']['total_checks'],
      ),
      'avg_max_age' => $this->calculateAverage(
        $metrics['cacheability']['totals']['max_age_sum'],
        $metrics['cacheability']['totals']['max_age_count'],
      ),
    ];

    $metrics['invalidations']['rate_per_hour'] = $this->calculatePerHourRate(
      $metrics['invalidations']['total_events'],
      $range,
    );

    $metrics['efficiency_score'] = $this->buildEfficiencyScore($metrics);
    $metrics['insights'] = $this->buildInsights($metrics);

    return $metrics;
  }

  /**
   * Aggregates request-level cache metadata from synthetic HTTP summary spans.
   *
   * @param array $metrics
   *   The metrics payload, passed by reference.
   * @param \Drupal\native_observability_dashboard\ValueObject\TimeRange $range
   *   The selected dashboard time range.
   */
  private function aggregateRequestSummarySpans(array &$metrics, TimeRange $range): void {
    if (!$this->database->schema()->tableExists(self::SPANS_TABLE)) {
      return;
    }

    $query = $this->database->select(self::SPANS_TABLE, 's')
      ->fields('s', ['id', 'payload'])
      ->condition('s.name', 'http.request_summary')
      ->condition('s.category', 'http')
      ->condition('s.started_at_ms', $range->getStartTimestamp() * 1000, '>=')
      ->condition('s.started_at_ms', $range->getEndTimestamp() * 1000, '<=')
      ->orderBy('s.id', 'DESC')
      ->range(0, self::MAX_REQUEST_SUMMARY_SPANS);

    foreach ($query->execute() as $row) {
      $payload = $this->decodePayload($row->payload ?? NULL);
      if ($payload === []) {
        continue;
      }

      $cache = is_array($payload['cache'] ?? NULL) ? $payload['cache'] : [];
      if ($cache === []) {
        continue;
      }

      $metrics['availability']['has_events'] = TRUE;
      $metrics['availability']['event_count']++;

      $hasResponseSignal = FALSE;

      $pageCacheStatus = strtoupper(trim((string) ($cache['page_cache_status'] ?? '')));
      if ($pageCacheStatus !== '') {
        $metrics['signals']['response_cache_headers_detected'] = TRUE;
        $metrics['summary']['response_cache_events']++;
        $this->applyBehaviorEvent($metrics['behavior']['page_cache'], $pageCacheStatus);
        $hasResponseSignal = TRUE;
      }

      $dynamicPageCacheStatus = strtoupper(trim((string) ($cache['dynamic_page_cache_status'] ?? '')));
      if ($dynamicPageCacheStatus !== '') {
        $metrics['signals']['response_cache_headers_detected'] = TRUE;
        $metrics['summary']['response_cache_events']++;
        $this->applyBehaviorEvent($metrics['behavior']['dynamic_page_cache'], $dynamicPageCacheStatus);
        $hasResponseSignal = TRUE;
      }

      if ($hasResponseSignal) {
        $metrics['behavior']['request_count']++;
      }

      $cacheabilityStatus = strtoupper(trim((string) ($cache['cacheability_status'] ?? '')));
      if ($cacheabilityStatus !== '') {
        $metrics['signals']['cacheability_detected'] = TRUE;
        $metrics['summary']['cacheability_events']++;
        $metrics['cacheability']['total_checks']++;

        $tagsCount = max(0, (int) ($cache['tags_count'] ?? 0));
        $contextsCount = max(0, (int) ($cache['contexts_count'] ?? 0));
        $maxAge = array_key_exists('max_age', $cache) ? (int) $cache['max_age'] : NULL;

        $metrics['cacheability']['totals']['tags'] += $tagsCount;
        $metrics['cacheability']['totals']['contexts'] += $contextsCount;

        if ($maxAge !== NULL) {
          $metrics['cacheability']['totals']['max_age_sum'] += max(0, $maxAge);
          $metrics['cacheability']['totals']['max_age_count']++;
        }

        if ($cacheabilityStatus === 'CACHEABLE') {
          $metrics['cacheability']['cacheable_count']++;
        }
        elseif ($cacheabilityStatus === 'UNCACHEABLE' || $maxAge === 0) {
          $metrics['cacheability']['uncacheable_count']++;
        }
        else {
          $metrics['cacheability']['unknown_count']++;
        }

        $this->applyMaxAgeDistribution($metrics['cacheability']['distributions']['max_age'], $maxAge);
        $this->applyCountDistribution($metrics['cacheability']['distributions']['tags'], $tagsCount, [0, 5, 10]);
        $this->applyCountDistribution($metrics['cacheability']['distributions']['contexts'], $contextsCount, [0, 3, 8]);
      }
    }
  }

  /**
   * Aggregates invalidation events from the cache observer event table.
   *
   * @param array $metrics
   *   The metrics payload, passed by reference.
   * @param \Drupal\native_observability_dashboard\ValueObject\TimeRange $range
   *   The selected dashboard time range.
   */
  private function aggregateInvalidationEvents(array &$metrics, TimeRange $range): void {
    if (!$this->database->schema()->tableExists(self::CACHE_EVENT_TABLE)) {
      return;
    }

    $query = $this->database->select(self::CACHE_EVENT_TABLE, 'e')
      ->fields('e', ['tag_count'])
      ->condition('e.event_type', 'tag_invalidation')
      ->condition('e.created_at', $range->getStartTimestamp(), '>=')
      ->condition('e.created_at', $range->getEndTimestamp(), '<=')
      ->orderBy('e.id', 'DESC');

    foreach ($query->execute() as $row) {
      $metrics['availability']['has_events'] = TRUE;
      $metrics['availability']['event_count']++;
      $metrics['signals']['invalidations_detected'] = TRUE;
      $metrics['summary']['invalidation_events']++;
      $metrics['invalidations']['total_events']++;
      $metrics['invalidations']['invalidated_tags'] += max(0, (int) ($row->tag_count ?? 0));
    }
  }

  /**
   * Decodes a span payload into a normalized PHP array.
   *
   * @param mixed $payload
   *   The raw payload value.
   *
   * @return array
   *   The decoded payload or an empty array.
   */
  private function decodePayload(mixed $payload): array {
    if (!is_string($payload) || $payload === '') {
      return [];
    }

    $decoded = json_decode($payload, TRUE);

    return is_array($decoded) ? $decoded : [];
  }

  /**
   * Applies a response cache event to a single behavior bucket.
   *
   * @param array $bucket
   *   The behavior bucket, passed by reference.
   * @param string $status
   *   The observed cache status.
   */
  private function applyBehaviorEvent(array &$bucket, string $status): void {
    if ($status === 'HIT') {
      $bucket['hits']++;
      $bucket['total']++;
      return;
    }

    if ($status === 'MISS') {
      $bucket['misses']++;
      $bucket['total']++;
      return;
    }

    if (str_contains($status, 'UNCACHEABLE')) {
      $bucket['uncacheable']++;
      $bucket['total']++;
      return;
    }

    if (str_contains($status, 'BYPASS')) {
      $bucket['bypass']++;
      $bucket['total']++;
      return;
    }

    $bucket['other']++;
    $bucket['total']++;
  }

  /**
   * Applies max-age distribution buckets.
   *
   * @param array $distribution
   *   The target distribution, passed by reference.
   * @param int|null $max_age
   *   The max-age value.
   */
  private function applyMaxAgeDistribution(array &$distribution, ?int $max_age): void {
    if ($max_age === NULL) {
      $distribution['unknown']++;
      return;
    }

    if ($max_age < 0) {
      $distribution['permanent']++;
      return;
    }

    if ($max_age === 0) {
      $distribution['zero']++;
      return;
    }

    if ($max_age <= 300) {
      $distribution['low']++;
      return;
    }

    if ($max_age <= 3600) {
      $distribution['medium']++;
      return;
    }

    $distribution['high']++;
  }

  /**
   * Applies a simple count distribution using inclusive thresholds.
   *
   * @param array $distribution
   *   The target distribution, passed by reference.
   * @param int $value
   *   The numeric value to distribute.
   * @param array $thresholds
   *   Three thresholds used to build four buckets.
   */
  private function applyCountDistribution(array &$distribution, int $value, array $thresholds): void {
    if ($value <= $thresholds[0]) {
      $distribution['none']++;
      return;
    }

    if ($value <= $thresholds[1]) {
      $distribution['low']++;
      return;
    }

    if ($value <= $thresholds[2]) {
      $distribution['medium']++;
      return;
    }

    $distribution['high']++;
  }

  /**
   * Finalizes the computed values for a behavior bucket.
   *
   * @param array $bucket
   *   The behavior bucket, passed by reference.
   */
  private function finalizeBehaviorMetrics(array &$bucket): void {
    $bucket['hit_rate'] = $this->calculateRatio($bucket['hits'], $bucket['total']);
    $bucket['miss_rate'] = $this->calculateRatio($bucket['misses'], $bucket['total']);
    $bucket['signals_detected'] = $bucket['total'] > 0;
  }

  /**
   * Builds the overall behavior metrics across all cache layers.
   *
   * @param array $page_cache
   *   The page cache behavior metrics.
   * @param array $dynamic_cache
   *   The dynamic page cache behavior metrics.
   *
   * @return array
   *   The overall behavior metrics.
   */
  private function buildOverallBehaviorMetrics(array $page_cache, array $dynamic_cache): array {
    $hits = (int) $page_cache['hits'] + (int) $dynamic_cache['hits'];
    $misses = (int) $page_cache['misses'] + (int) $dynamic_cache['misses'];
    $uncacheable = (int) $page_cache['uncacheable'] + (int) $dynamic_cache['uncacheable'];
    $bypass = (int) $page_cache['bypass'] + (int) $dynamic_cache['bypass'];
    $other = (int) $page_cache['other'] + (int) $dynamic_cache['other'];
    $total = $hits + $misses + $uncacheable + $bypass + $other;

    return [
      'hits' => $hits,
      'misses' => $misses,
      'uncacheable' => $uncacheable,
      'bypass' => $bypass,
      'other' => $other,
      'total' => $total,
      'hit_rate' => $this->calculateRatio($hits, $total),
      'miss_rate' => $this->calculateRatio($misses, $total),
      'signals_detected' => $total > 0,
    ];
  }

  /**
   * Builds the cache efficiency score payload.
   *
   * @param array $metrics
   *   The aggregated metrics payload.
   *
   * @return array
   *   The normalized score payload.
   */
  private function buildEfficiencyScore(array $metrics): array {
    $score = 0;
    $breakdown = [];

    $cacheableRatio = (float) ($metrics['cacheability']['ratios']['cacheable_ratio'] ?? 0.0);
    $httpCacheActive = (bool) ($metrics['signals']['http_cache_activity_detected'] ?? FALSE);
    $invalidationRate = (float) ($metrics['invalidations']['rate_per_hour'] ?? 0.0);
    $dynamicMissRate = (float) ($metrics['behavior']['dynamic_page_cache']['miss_rate'] ?? 0.0);

    $cacheablePoints = (int) round(min(30, ($cacheableRatio / 100) * 30));
    $score += $cacheablePoints;
    $breakdown[] = [
      'label' => $this->t('Cacheable responses'),
      'value' => $cacheablePoints,
      'max' => 30,
      'status' => $cacheableRatio >= 60 ? 'ok' : 'warning',
    ];

    $httpCachePoints = $httpCacheActive ? 30 : 0;
    $score += $httpCachePoints;
    $breakdown[] = [
      'label' => $this->t('HTTP cache activity'),
      'value' => $httpCachePoints,
      'max' => 30,
      'status' => $httpCacheActive ? 'ok' : 'warning',
    ];

    if ($invalidationRate <= 5) {
      $invalidationPoints = 20;
      $invalidationStatus = 'ok';
    }
    elseif ($invalidationRate <= 15) {
      $invalidationPoints = 12;
      $invalidationStatus = 'warning';
    }
    else {
      $invalidationPoints = 4;
      $invalidationStatus = 'critical';
    }

    $score += $invalidationPoints;
    $breakdown[] = [
      'label' => $this->t('Invalidation pressure'),
      'value' => $invalidationPoints,
      'max' => 20,
      'status' => $invalidationStatus,
    ];

    if ($dynamicMissRate <= 20) {
      $dynamicPoints = 20;
      $dynamicStatus = 'ok';
    }
    elseif ($dynamicMissRate <= 60) {
      $dynamicPoints = 10;
      $dynamicStatus = 'warning';
    }
    else {
      $dynamicPoints = 4;
      $dynamicStatus = 'critical';
    }

    $score += $dynamicPoints;
    $breakdown[] = [
      'label' => $this->t('Dynamic cache effectiveness'),
      'value' => $dynamicPoints,
      'max' => 20,
      'status' => $dynamicStatus,
    ];

    return [
      'value' => max(0, min(100, $score)),
      'max' => 100,
      'status' => $score >= 75 ? 'ok' : ($score >= 45 ? 'warning' : 'critical'),
      'breakdown' => $breakdown,
    ];
  }

  /**
   * Builds intelligent insights from the aggregated metrics.
   *
   * @param array $metrics
   *   The aggregated metrics payload.
   *
   * @return array
   *   A list of cache insights.
   */
  private function buildInsights(array $metrics): array {
    $insights = [];

    if (
      !empty($metrics['signals']['cacheability_detected'])
      && empty($metrics['signals']['http_cache_activity_detected'])
    ) {
      $insights[] = [
        'status' => 'warning',
        'title' => $this->t('Cacheable responses detected without cache status signals'),
        'description' => $this->t('Cacheability metadata was recorded, but no page or dynamic cache hit/miss signals were detected in the selected range.'),
      ];
    }

    if (($metrics['invalidations']['rate_per_hour'] ?? 0.0) > 15) {
      $insights[] = [
        'status' => 'warning',
        'title' => $this->t('High invalidation rate detected'),
        'description' => $this->t('Frequent invalidations may reduce cache efficiency and lead to unstable runtime cache behavior.'),
      ];
    }

    if (($metrics['cacheability']['ratios']['uncacheable_ratio'] ?? 0.0) >= 50) {
      $insights[] = [
        'status' => 'warning',
        'title' => $this->t('A large share of responses is uncacheable'),
        'description' => $this->t('More than half of the observed request summaries were marked as uncacheable or carried a zero max-age value.'),
      ];
    }

    if (($metrics['behavior']['dynamic_page_cache']['miss_rate'] ?? 0.0) >= 60) {
      $insights[] = [
        'status' => 'info',
        'title' => $this->t('Dynamic page cache currently shows a high miss ratio'),
        'description' => $this->t('Observed request summaries indicate that dynamic page cache is mostly serving misses in the selected range.'),
      ];
    }

    if (
      empty($metrics['signals']['response_cache_headers_detected'])
      && empty($metrics['signals']['cacheability_detected'])
      && empty($metrics['signals']['invalidations_detected'])
    ) {
      $insights[] = [
        'status' => 'info',
        'title' => $this->t('No cache observability signals detected'),
        'description' => $this->t('The selected range does not currently contain request summary cache signals or cache invalidation events.'),
      ];
    }

    return $insights;
  }

  /**
   * Calculates a safe ratio value between 0 and 100.
   *
   * @param int|float $numerator
   *   The numerator.
   * @param int|float $denominator
   *   The denominator.
   *
   * @return float
   *   The normalized ratio.
   */
  private function calculateRatio(int|float $numerator, int|float $denominator): float {
    if ($denominator <= 0) {
      return 0.0;
    }

    return round(($numerator / $denominator) * 100, 2);
  }

  /**
   * Calculates a safe average value.
   *
   * @param int|float $sum
   *   The sum of all values.
   * @param int $count
   *   The number of values.
   *
   * @return float
   *   The computed average.
   */
  private function calculateAverage(int|float $sum, int $count): float {
    if ($count <= 0) {
      return 0.0;
    }

    return round($sum / $count, 2);
  }

  /**
   * Calculates the invalidation rate per hour.
   *
   * @param int $count
   *   The total invalidation count.
   * @param \Drupal\native_observability_dashboard\ValueObject\TimeRange $range
   *   The selected time range.
   *
   * @return float
   *   The normalized invalidation rate per hour.
   */
  private function calculatePerHourRate(int $count, TimeRange $range): float {
    $hours = max(1, $range->getDurationInSeconds() / 3600);

    return round($count / $hours, 2);
  }

  /**
   * Builds the default empty metrics structure.
   *
   * @return array
   *   The empty metrics payload.
   */
  private function buildEmptyMetrics(): array {
    return [
      'availability' => [
        'has_events' => FALSE,
        'event_count' => 0,
      ],
      'summary' => [
        'total_events' => 0,
        'response_cache_events' => 0,
        'cacheability_events' => 0,
        'invalidation_events' => 0,
      ],
      'signals' => [
        'response_cache_headers_detected' => FALSE,
        'cacheability_detected' => FALSE,
        'invalidations_detected' => FALSE,
        'http_cache_activity_detected' => FALSE,
      ],
      'behavior' => [
        'request_count' => 0,
        'page_cache' => [
          'hits' => 0,
          'misses' => 0,
          'uncacheable' => 0,
          'bypass' => 0,
          'other' => 0,
          'total' => 0,
          'hit_rate' => 0.0,
          'miss_rate' => 0.0,
          'signals_detected' => FALSE,
        ],
        'dynamic_page_cache' => [
          'hits' => 0,
          'misses' => 0,
          'uncacheable' => 0,
          'bypass' => 0,
          'other' => 0,
          'total' => 0,
          'hit_rate' => 0.0,
          'miss_rate' => 0.0,
          'signals_detected' => FALSE,
        ],
        'overall' => [
          'hits' => 0,
          'misses' => 0,
          'uncacheable' => 0,
          'bypass' => 0,
          'other' => 0,
          'total' => 0,
          'hit_rate' => 0.0,
          'miss_rate' => 0.0,
          'signals_detected' => FALSE,
        ],
      ],
      'cacheability' => [
        'total_checks' => 0,
        'cacheable_count' => 0,
        'uncacheable_count' => 0,
        'unknown_count' => 0,
        'ratios' => [
          'cacheable_ratio' => 0.0,
          'uncacheable_ratio' => 0.0,
        ],
        'averages' => [
          'avg_tags' => 0.0,
          'avg_contexts' => 0.0,
          'avg_max_age' => 0.0,
        ],
        'totals' => [
          'tags' => 0,
          'contexts' => 0,
          'max_age_sum' => 0,
          'max_age_count' => 0,
        ],
        'distributions' => [
          'max_age' => [
            'unknown' => 0,
            'permanent' => 0,
            'zero' => 0,
            'low' => 0,
            'medium' => 0,
            'high' => 0,
          ],
          'tags' => [
            'none' => 0,
            'low' => 0,
            'medium' => 0,
            'high' => 0,
          ],
          'contexts' => [
            'none' => 0,
            'low' => 0,
            'medium' => 0,
            'high' => 0,
          ],
        ],
      ],
      'invalidations' => [
        'total_events' => 0,
        'invalidated_tags' => 0,
        'rate_per_hour' => 0.0,
      ],
      'efficiency_score' => [
        'value' => 0,
        'max' => 100,
        'status' => 'warning',
        'breakdown' => [],
      ],
      'insights' => [],
    ];
  }

}
