<?php

declare(strict_types=1);

namespace Drupal\native_observability_execution\Service;

use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Pager\PagerManagerInterface;
use Drupal\Core\TempStore\PrivateTempStoreFactory;

/**
 * Builds a basic render array for an execution timeline.
 *
 * Phase 2 MVP:
 * - Prefer spans collected in the current request (SpanStack)
 * - Fallback to recently persisted spans (PrivateTempStore) to allow
 *   cross-request inspection (navigate -> then open Execution page)
 */
final class ExecutionTimelineBuilder {
  use StringTranslationTrait;

  /**
   * TempStore collection and key used by the request subscriber.
   */
  private const STORE_COLLECTION = 'native_observability_execution';
  private const STORE_KEY_RECENT_SPANS = 'recent_request_spans';

  public function __construct(
    private readonly ExecutionSpanStack $span_stack,
    private readonly PrivateTempStoreFactory $tempStoreFactory,
    private readonly PagerManagerInterface $pagerManager,
  ) {
  }

  /**
   * Builds the timeline render array.
   *
   * @return array
   *   A render array.
   */
  public function build(): array {
    $spans = $this->span_stack->all();

    // If the current request has no spans, fallback to the last recorded ones.
    if (!$spans) {
      $store = $this->tempStoreFactory->get(self::STORE_COLLECTION);
      $recent = $store->get(self::STORE_KEY_RECENT_SPANS);
      if (is_array($recent) && $recent) {
        $spans = $recent;
      }
    }

    if (!$spans) {
      return [
        '#type' => 'markup',
        '#markup' => '<p><em>' . $this->t('No spans recorded yet.') . '</em></p>',
      ];
    }

    $rows = [];
    foreach ($spans as $span) {
      $rows[] = [
        (string) ($span['method'] ?? $this->t('n/a')),
        (string) ($span['path'] ?? $this->t('n/a')),
        (string) ($span['status'] ?? $this->t('n/a')),
        (string) ($span['duration_ms'] ?? $this->t('n/a')),
        (string) ($span['category'] ?? $this->t('n/a')),
      ];
    }

    return [
      '#type' => 'table',
      '#header' => [
        $this->t('Method'),
        $this->t('Path'),
        $this->t('Status'),
        $this->t('Duration (ms)'),
        $this->t('Category'),
      ],
      '#rows' => $rows,
      '#empty' => $this->t('No spans recorded.'),
    ];
  }

  /**
   * Builds the timeline table using query filters.
   *
   * @param array<string, mixed> $query
   *   Raw query parameters (GET).
   */
  public function buildFromQuery(array $query): array {
    $spans = $this->loadSpansWithFallback();
    // Default items per page (can be made configurable later).
    $limit = 20;

    $path = isset($query['path']) ? (string) $query['path'] : '';
    $method = isset($query['method']) ? (string) $query['method'] : '';
    $status = ($query['status'] ?? '') !== '' ? (int) $query['status'] : NULL;
    $category = isset($query['category']) ? (string) $query['category'] : '';

    $sort = isset($query['sort']) ? (string) $query['sort'] : 'ts';
    $order = isset($query['order']) ? (string) $query['order'] : 'desc';
    $order = in_array($order, ['asc', 'desc'], TRUE) ? $order : 'desc';

    // Filter.
    $spans = array_values(array_filter($spans, static function (array $span) use ($path, $method, $status, $category): bool {
      if ($path !== '' && stripos((string) ($span['path'] ?? ''), $path) === FALSE) {
        return FALSE;
      }
      if ($method !== '' && strtoupper((string) ($span['method'] ?? '')) !== strtoupper($method)) {
        return FALSE;
      }
      if ($status !== NULL && (int) ($span['status'] ?? -1) !== $status) {
        return FALSE;
      }
      if ($category !== '' && stripos((string) ($span['category'] ?? ''), $category) === FALSE) {
        return FALSE;
      }
      return TRUE;
    }));

    $total = count($spans);
    $pager = $this->pagerManager->createPager($total, $limit);
    $current_page = $pager->getCurrentPage();
    $offset = $current_page * $limit;

    $spans = array_slice($spans, $offset, $limit);

    // Sort.
    usort($spans, static function (array $a, array $b) use ($sort, $order): int {
      $dir = $order === 'asc' ? 1 : -1;

      $va = match ($sort) {
        'duration' => (int) ($a['duration_ms'] ?? 0),
        default => (int) ($a['ts'] ?? 0),
      };

      $vb = match ($sort) {
        'duration' => (int) ($b['duration_ms'] ?? 0),
        default => (int) ($b['ts'] ?? 0),
      };

      // Multiply by $dir to flip ordering.
      return ($va <=> $vb) * $dir;
    });

    // Limit.
    $spans = array_slice($spans, 0, $limit);

    return $this->renderTable($spans);
  }

  /**
   * Loads spans from the current request, falling back to TempStore.
   *
   * @return array<int, array<string, mixed>>
   *   The spans list.
   */
  private function loadSpansWithFallback(): array {
    $spans = $this->span_stack->all();
    if ($spans) {
      return $spans;
    }

    $store = $this->tempStoreFactory->get(self::STORE_COLLECTION);
    $recent = $store->get(self::STORE_KEY_RECENT_SPANS);
    return (is_array($recent) ? $recent : []);
  }

  /**
   * Renders a table for the given spans.
   *
   * @param array<int, array<string, mixed>> $spans
   *   The spans list.
   */
  private function renderTable(array $spans): array {
    if (!$spans) {
      return [
        '#type' => 'markup',
        '#markup' => '<p><em>' . $this->t('No spans recorded yet.') . '</em></p>',
      ];
    }

    // Web-style performance bar (not a real offset timeline).
    // We normalize duration on a fixed scale so the bar is always meaningful.
    // This keeps the UI immediately readable with green, orange, and red
    // thresholds similar to common web performance indicators.
    $max_ms = 500;

    $rows = [];
    foreach ($spans as $span) {
      $dur = (int) ($span['duration_ms'] ?? 0);

      // Always start from the left for a progress bar visualization.
      $left_pct = 0.0;

      // Map duration to 0..100 width based on the configured scale.
      $width_pct = min(100.0, ($dur / $max_ms) * 100.0);

      // Ensure minimum visibility for non-zero durations.
      if ($dur > 0) {
        $width_pct = max(8.0, $width_pct);
      }

      // Determine color class based on web-style thresholds (MVP).
      $status_class = 'no-exec-fast';
      if ($dur >= 500) {
        $status_class = 'no-exec-slow';
      }
      elseif ($dur >= 200) {
        $status_class = 'no-exec-mid';
      }

      $bar = [
        '#theme' => 'native_observability_execution_timeline_bar',
        '#left_pct' => (float) $left_pct,
        '#width_pct' => (float) $width_pct,
        '#status_class' => $status_class,
        '#duration_ms' => (int) $dur,
        '#category' => (string) ($span['category'] ?? ''),
        '#attached' => [
          'library' => [
            'native_observability_execution/execution_timeline',
          ],
        ],
      ];

      $rows[] = [
        (string) ($span['method'] ?? $this->t('n/a')),
        (string) ($span['path'] ?? $this->t('n/a')),
        (string) ($span['status'] ?? $this->t('n/a')),
        (string) ($span['duration_ms'] ?? $this->t('n/a')),
        (string) ($span['category'] ?? $this->t('n/a')),
        ['data' => $bar],
      ];
    }

    return [
      'table' => [
        '#type' => 'table',
        '#header' => [
          $this->t('Method'),
          $this->t('Path'),
          $this->t('Status'),
          $this->t('Duration (ms)'),
          $this->t('Category'),
          $this->t('Timeline'),
        ],
        '#rows' => $rows,
        '#empty' => $this->t('No spans recorded.'),
        '#attached' => [
          'library' => [
            'native_observability_execution/execution_timeline',
          ],
        ],
      ],
      'pager' => [
        '#type' => 'pager',
      ],
    ];
  }

  /**
   * Returns distinct HTTP methods found in recent spans.
   *
   * @return array<string, string>
   *   An associative array suitable for a select options list.
   */
  public function getAvailableMethods(): array {
    $spans = $this->loadSpansWithFallback();

    $methods = [];
    foreach ($spans as $span) {
      if (!empty($span['method'])) {
        $methods[strtoupper($span['method'])] = strtoupper($span['method']);
      }
    }

    ksort($methods);

    return $methods;
  }

  /**
   * Returns distinct status codes found in recent spans.
   *
   * @return array<string, string>
   *   An associative array suitable for a select options list.
   */
  public function getAvailableStatuses(): array {
    $spans = $this->loadSpansWithFallback();

    $statuses = [];
    foreach ($spans as $span) {
      if (isset($span['status'])) {
        $code = (string) $span['status'];
        $statuses[$code] = $code;
      }
    }
    ksort($statuses);
    return $statuses;
  }

  /**
   * Returns the list of available span categories from the current dataset.
   *
   * This is used to populate the Category filter dropdown, so users can only
   * select values that are actually present in results.
   *
   * @return array<string, string>
   *   An associative array of category => category labels.
   */
  public function getAvailableCategories(): array {
    $spans = $this->loadSpansWithFallback();

    $categories = [];
    foreach ($spans as $span) {
      $category = isset($span['category']) ? (string) $span['category'] : '';
      if ($category === '') {
        continue;
      }
      $categories[$category] = $category;
    }

    ksort($categories);

    return $categories;
  }

}
