<?php

declare(strict_types=1);

namespace Drupal\native_observability_spans\Service;

use Drupal\native_observability\EventSubscriber\TraceSubscriber;
use Drupal\native_observability\Service\RequestIdGeneratorInterface;
use Drupal\native_observability\Service\RequestLogExclusionMatcher;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Default span recorder implementation.
 *
 * Stores span start timestamps in the current request attributes so
 * endSpan() can compute durations without an extra DB lookup.
 */
final class SpanRecorder implements SpanRecorderInterface {

  /**
   * Creates a new span recorder instance.
   */
  public function __construct(
    private readonly RequestIdGeneratorInterface $idGenerator,
    private readonly SpanStorageInterface $storage,
    private readonly RequestStack $requestStack,
    private readonly ActiveSpanContextStack $activeSpanContextStack,
  ) {
  }

  /**
   * {@inheritdoc}
   */
  public function startSpan(string $name, ?string $category = NULL, ?string $parent_span_id = NULL, array $payload = []): string {
    $request = $this->requestStack->getCurrentRequest();

    if (RequestLogExclusionMatcher::isRequestExcluded($request)) {
      return '';
    }

    $requestId = $request
      ? (string) ($request->attributes->get(TraceSubscriber::ATTR_REQUEST_ID) ?? '')
      : '';

    $resolvedParentSpanId = $parent_span_id ?: $this->activeSpanContextStack->getCurrentSpanId();
    $spanId = $this->idGenerator->generate();
    $startedAtMs = (int) round(microtime(TRUE) * 1000);

    try {
      $this->storage->insert([
        'trace_id' => 0,
        'request_id' => $requestId,
        'span_id' => $spanId,
        'parent_span_id' => $resolvedParentSpanId,
        'name' => $name,
        'category' => $category,
        'started_at_ms' => $startedAtMs,
        'ended_at_ms' => NULL,
        'duration_ms' => NULL,
        'payload' => $payload ? json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : NULL,
      ]);
    }
    catch (\Throwable) {
      // Fail-safe: never break execution flow for observability.
    }

    if ($request) {
      $request->attributes->set('native_observability.span_started.' . $spanId, $startedAtMs);
    }

    $this->activeSpanContextStack->push($spanId);

    return $spanId;
  }

  /**
   * {@inheritdoc}
   */
  public function endSpan(string $span_id, array $payload = []): void {
    if ($span_id === '') {
      return;
    }

    $request = $this->requestStack->getCurrentRequest();

    if (RequestLogExclusionMatcher::isRequestExcluded($request)) {
      return;
    }

    $startedAtMs = NULL;
    if ($request) {
      $startedAtMs = $request->attributes->get('native_observability.span_started.' . $span_id);
    }

    $endedAtMs = (int) round(microtime(TRUE) * 1000);
    $durationMs = $startedAtMs !== NULL ? max(0, $endedAtMs - (int) $startedAtMs) : 0;

    try {
      $this->storage->end(
        $span_id,
        $endedAtMs,
        $durationMs,
        $payload ? json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) : NULL,
      );
    }
    catch (\Throwable) {
      // Fail-safe.
    }

    $this->activeSpanContextStack->pop($span_id);
  }

}
