<?php

declare(strict_types=1);

namespace Drupal\native_observability_spans\Service;

use Drupal\Core\Database\Connection;

/**
 * Database-based span storage implementation.
 *
 * Notes:
 * - Uses Schema API table "native_observability_spans".
 * - Avoids Entity API intentionally (performance + minimalism).
 * - All operations must be fail-safe for runtime calls.
 */
final class DatabaseSpanStorage implements SpanStorageInterface {

  /**
   * The DB table name for spans.
   */
  private const TABLE = 'native_observability_spans';

  public function __construct(
    private readonly Connection $database,
  ) {
  }

  /**
   * {@inheritdoc}
   */
  public function insert(array $span): int {
    $span += [
      'trace_id' => 0,
      'request_id' => '',
      'span_id' => '',
      'parent_span_id' => NULL,
      'name' => '',
      'category' => NULL,
      'started_at_ms' => 0,
      'ended_at_ms' => NULL,
      'duration_ms' => NULL,
      'payload' => NULL,
    ];

    return (int) $this->database
      ->insert(self::TABLE)
      ->fields([
        'trace_id' => (int) $span['trace_id'],
        'request_id' => (string) $span['request_id'],
        'span_id' => (string) $span['span_id'],
        'parent_span_id' => $span['parent_span_id'] !== '' ? $span['parent_span_id'] : NULL,
        'name' => (string) $span['name'],
        'category' => $span['category'] !== '' ? $span['category'] : NULL,
        'started_at_ms' => (int) $span['started_at_ms'],
        'ended_at_ms' => $span['ended_at_ms'] !== NULL ? (int) $span['ended_at_ms'] : NULL,
        'duration_ms' => $span['duration_ms'] !== NULL ? (int) $span['duration_ms'] : NULL,
        'payload' => $span['payload'],
      ])
      ->execute();
  }

  /**
   * {@inheritdoc}
   */
  public function end(string $span_id, int $ended_at_ms, int $duration_ms, ?string $payload_json = NULL): void {
    $fields = [
      'ended_at_ms' => $ended_at_ms,
      'duration_ms' => $duration_ms,
    ];
    if ($payload_json !== NULL) {
      $fields['payload'] = $payload_json;
    }

    $this->database
      ->update(self::TABLE)
      ->fields($fields)
      ->condition('span_id', $span_id)
      ->execute();
  }

  /**
   * {@inheritdoc}
   */
  public function attachTraceId(string $request_id, int $trace_id): void {
    if (!$this->database->schema()->fieldExists(self::TABLE, 'request_id')) {
      return;
    }

    $this->database
      ->update(self::TABLE)
      ->fields(['trace_id' => $trace_id])
      ->condition('request_id', $request_id)
      ->condition('trace_id', 0)
      ->execute();
  }

  /**
   * {@inheritdoc}
   */
  public function loadByTraceId(int $trace_id): array {
    $result = $this->database
      ->select(self::TABLE, 's')
      ->fields('s')
      ->condition('trace_id', $trace_id)
      ->orderBy('started_at_ms', 'ASC')
      ->execute();

    return $result->fetchAll(\PDO::FETCH_ASSOC) ?: [];
  }

  /**
   * {@inheritdoc}
   */
  public function loadByRequestId(string $request_id): array {
    if (!$this->database->schema()->fieldExists(self::TABLE, 'request_id')) {
      return [];
    }

    $result = $this->database
      ->select(self::TABLE, 's')
      ->fields('s')
      ->condition('request_id', $request_id)
      ->orderBy('started_at_ms', 'ASC')
      ->execute();

    return $result->fetchAll(\PDO::FETCH_ASSOC) ?: [];
  }

  /**
   * {@inheritdoc}
   */
  public function deleteByRequestId(string $request_id): int {
    if (!$this->database->schema()->fieldExists(self::TABLE, 'request_id')) {
      return 0;
    }

    return (int) $this->database
      ->delete('native_observability_spans')
      ->condition('request_id', $request_id)
      ->execute();
  }

  /**
   * {@inheritdoc}
   */
  public function deleteAll(): int {
    return (int) $this->database
      ->delete('native_observability_spans')
      ->execute();
  }

}
