<?php

declare(strict_types=1);

namespace Drupal\native_observability_execution\Service;

/**
 * Default span recorder implementation.
 */
final readonly class ExecutionSpanRecorder implements ExecutionSpanRecorderInterface {

  public function __construct(
    private ExecutionSpanStack $stack,
  ) {
  }

  /**
   * Records an execution span in the in-memory stack.
   *
   * @param string $category
   *   The span category.
   * @param string $name
   *   The span name.
   * @param int $duration_ms
   *   The duration in milliseconds.
   * @param array<string, mixed> $meta
   *   Optional span metadata.
   */
  public function record(string $category, string $name, int $duration_ms, array $meta = []): void {
    $span = [
      'category' => $category,
      'name' => $name,
      'duration_ms' => $duration_ms,
      'ts' => (int) round(microtime(TRUE) * 1000),
      'meta' => $meta,
    ];

    // Promote common keys to top-level so the Execution table can render them.
    if (isset($meta['method'])) {
      $span['method'] = (string) $meta['method'];
    }

    if (isset($meta['path'])) {
      $path = (string) $meta['path'];
      $span['path'] = $path !== '' ? $path : '/';
    }

    if (isset($meta['status'])) {
      $span['status'] = (int) $meta['status'];
    }

    $this->stack->add($span);
  }

  /**
   * Measures a callable and records its execution span.
   *
   * @param string $category
   *   The span category.
   * @param string $name
   *   The span name.
   * @param callable $callback
   *   The callback to measure.
   * @param array<string, mixed> $meta
   *   Optional span metadata.
   *
   * @return mixed
   *   The callback result.
   *
   * @throws \Throwable
   *   Re-throws any callback exception after recording the failure.
   */
  public function measure(string $category, string $name, callable $callback, array $meta = []) {
    $start = microtime(TRUE);

    try {
      $result = $callback();
      $duration_ms = (int) round((microtime(TRUE) - $start) * 1000);

      $this->record($category, $name, $duration_ms, $meta + ['outcome' => 'ok']);

      return $result;
    }
    catch (\Throwable $e) {
      $duration_ms = (int) round((microtime(TRUE) - $start) * 1000);

      $this->record($category, $name, $duration_ms, $meta + [
        'outcome' => 'error',
        'exception_class' => get_class($e),
        'exception_message' => $e->getMessage(),
      ]);

      throw $e;
    }
  }

}
