<?php

declare(strict_types=1);

namespace Drupal\native_observability_otel\Service;

use Drupal\Core\Config\ConfigFactoryInterface;

/**
 * Builds OTLP-compatible JSON payloads for exported traces.
 */
final class OpenTelemetryPayloadBuilder {

  /**
   * Constructs the payload builder.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   */
  public function __construct(
    private readonly ConfigFactoryInterface $config_factory,
  ) {
  }

  /**
   * Builds an OTLP-compatible trace payload.
   *
   * @param array $payload
   *   Internal trace payload.
   *
   * @return array
   *   OTLP-compatible JSON structure.
   */
  public function build(array $payload): array {
    $config = $this->config_factory->get('native_observability_otel.settings');

    $service_name = (string) ($config->get('service_name') ?: 'drupal');
    $service_namespace = (string) ($config->get('service_namespace') ?: 'native_observability');
    $service_version = (string) ($config->get('service_version') ?: '1.0.0');

    $trace_source = (string) ($payload['request_id'] ?? $payload['trace_id'] ?? uniqid('', TRUE));
    $span_source = $trace_source . '|' . ($payload['path'] ?? '') . '|' . ($payload['method'] ?? '');

    $trace_id = $this->normalizeTraceId($trace_source);
    $span_id = $this->normalizeSpanId($span_source);

    $ended_at = isset($payload['ended_at']) ? (float) $payload['ended_at'] : microtime(TRUE);
    $duration_ms = (float) ($payload['duration_ms'] ?? 0);
    $started_at = isset($payload['started_at'])
      ? (float) $payload['started_at']
      : max(0.0, $ended_at - ($duration_ms / 1000));

    $span_name = trim(sprintf(
      '%s %s',
      (string) ($payload['method'] ?? 'REQUEST'),
      (string) ($payload['route_name'] ?? $payload['path'] ?? '/')
    ));

    $status_code = (int) ($payload['status_code'] ?? 0);
    $otel_status_code = $status_code >= 400 ? 2 : 1;

    return [
      'resourceSpans' => [
        [
          'resource' => [
            'attributes' => [
              $this->buildStringAttribute('service.name', $service_name),
              $this->buildStringAttribute('service.namespace', $service_namespace),
              $this->buildStringAttribute('service.version', $service_version),
            ],
          ],
          'scopeSpans' => [
            [
              'scope' => [
                'name' => 'native_observability_otel',
                'version' => $service_version,
              ],
              'spans' => [
                [
                  'traceId' => $trace_id,
                  'spanId' => $span_id,
                  'name' => $span_name,
                  'kind' => 2,
                  'startTimeUnixNano' => $this->toUnixNano($started_at),
                  'endTimeUnixNano' => $this->toUnixNano($ended_at),
                  'attributes' => $this->buildSpanAttributes($payload),
                  'status' => [
                    'code' => $otel_status_code,
                  ],
                ],
              ],
            ],
          ],
        ],
      ],
    ];
  }

  /**
   * Builds span attributes from the internal payload.
   *
   * @param array $payload
   *   Internal trace payload.
   *
   * @return array
   *   OTLP span attributes.
   */
  private function buildSpanAttributes(array $payload): array {
    $attributes = [];

    if (!empty($payload['method'])) {
      $attributes[] = $this->buildStringAttribute('http.request.method', (string) $payload['method']);
    }

    if (!empty($payload['path'])) {
      $attributes[] = $this->buildStringAttribute('url.path', (string) $payload['path']);
    }

    if (!empty($payload['route_name'])) {
      $attributes[] = $this->buildStringAttribute('drupal.route_name', (string) $payload['route_name']);
    }

    if (isset($payload['status_code'])) {
      $attributes[] = $this->buildIntAttribute('http.response.status_code', (int) $payload['status_code']);
    }

    if (isset($payload['duration_ms'])) {
      $attributes[] = $this->buildDoubleAttribute('drupal.duration_ms', (float) $payload['duration_ms']);
    }

    if (isset($payload['uid'])) {
      $attributes[] = $this->buildIntAttribute('enduser.id', (int) $payload['uid']);
    }

    if (!empty($payload['client_ip'])) {
      $attributes[] = $this->buildStringAttribute('client.address', (string) $payload['client_ip']);
    }

    if (!empty($payload['request_id'])) {
      $attributes[] = $this->buildStringAttribute('drupal.request_id', (string) $payload['request_id']);
    }

    if (!empty($payload['exception']) && is_array($payload['exception'])) {
      if (!empty($payload['exception']['class'])) {
        $attributes[] = $this->buildStringAttribute('exception.type', (string) $payload['exception']['class']);
      }

      if (!empty($payload['exception']['message'])) {
        $attributes[] = $this->buildStringAttribute('exception.message', (string) $payload['exception']['message']);
      }

      if (isset($payload['exception']['code'])) {
        $attributes[] = $this->buildIntAttribute('exception.code', (int) $payload['exception']['code']);
      }
    }

    return $attributes;
  }

  /**
   * Builds a string attribute.
   */
  private function buildStringAttribute(string $key, string $value): array {
    return [
      'key' => $key,
      'value' => [
        'stringValue' => $value,
      ],
    ];
  }

  /**
   * Builds an integer attribute.
   */
  private function buildIntAttribute(string $key, int $value): array {
    return [
      'key' => $key,
      'value' => [
        'intValue' => (string) $value,
      ],
    ];
  }

  /**
   * Builds a double attribute.
   */
  private function buildDoubleAttribute(string $key, float $value): array {
    return [
      'key' => $key,
      'value' => [
        'doubleValue' => $value,
      ],
    ];
  }

  /**
   * Converts a unix timestamp in seconds to OTLP unix nano format.
   */
  private function toUnixNano(float $timestamp): string {
    return (string) (int) round($timestamp * 1000000000);
  }

  /**
   * Normalizes an arbitrary value into a valid 32-char trace ID.
   */
  private function normalizeTraceId(string $value): string {
    return substr(hash('sha256', $value), 0, 32);
  }

  /**
   * Normalizes an arbitrary value into a valid 16-char span ID.
   */
  private function normalizeSpanId(string $value): string {
    return substr(hash('sha256', $value), 0, 16);
  }

}
