<?php

declare(strict_types=1);

namespace Drupal\native_observability_eca_bridge\Plugin\ECA\Event;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\eca\Attribute\EcaEvent;
use Drupal\eca\Attribute\Token;
use Drupal\eca\Event\Tag;
use Drupal\eca\Plugin\ECA\Event\EventBase;
use Drupal\native_observability\Event\TraceRecordedEvent;
use Drupal\native_observability\Event\TracesDeletedEvent;
use Drupal\native_observability_eca_bridge\Eca\NativeObservabilityEventDefinitions;

/**
 * ECA event plugin exposing Native Observability trace signals.
 *
 * Provides two event variants:
 *
 * - trace_recorded: Fires after every HTTP trace is persisted. Gives access
 *   to route, method, status code, duration and AJAX flag via [event:*]
 *   tokens.
 *
 * - traces_deleted: Fires after a bulk trace purge. Gives access to the
 *   number of deleted records via [event:deleted_count].
 *
 * To react only to slow requests, add an ECA condition of type
 * "Scalar comparison" checking [event:duration_ms] >= threshold after
 * selecting the trace_recorded trigger. ECA's constraint is that each
 * Symfony event name must map to exactly one ECA plugin definition, so
 * threshold-based filtering must be expressed as a condition rather than
 * a separate event variant.
 *
 * Route exclusion is handled upstream: when capture.exclude_own_routes is
 * enabled, TraceSubscriber returns before calling storage, so the event is
 * never dispatched for own-module routes. This plugin needs no extra guard.
 *
 * @see \Drupal\native_observability\Event\TraceRecordedEvent
 * @see \Drupal\native_observability\Event\TracesDeletedEvent
 * @see \Drupal\native_observability\EventSubscriber\TraceSubscriber
 */
#[EcaEvent(
  id: 'native_observability',
  label: new TranslatableMarkup('Native Observability'),
  category: new TranslatableMarkup('Native Observability'),
  description: new TranslatableMarkup('Events dispatched by the Native Observability module when traces are recorded or deleted.'),
  deriver: NativeObservabilityEventDeriver::class,
  version_introduced: '1.0.0',
)]
class NativeObservabilityEvent extends EventBase {

  /**
   * Returns the set of event variants exposed by this plugin.
   *
   * Each entry corresponds to one ECA-selectable trigger. The deriver
   * inflates these definitions into individual plugin IDs.
   *
   * ECA requires that each Symfony event name appears in at most one plugin
   * definition. Threshold-based filtering (slow request detection) must
   * therefore be expressed as an ECA condition on [event:duration_ms], not
   * as a separate derivative sharing the same Symfony event name.
   *
   * @return array<string, array<string, mixed>>
   *   Keyed by derivative ID; each value is an ECA event definition array.
   */
  public static function definitions(): array {
    $definitions = NativeObservabilityEventDefinitions::definitions();
    $definitions['trace_recorded']['tags'] = Tag::READ | Tag::RUNTIME | Tag::AFTER;
    $definitions['traces_deleted']['tags'] = Tag::WRITE | Tag::RUNTIME | Tag::AFTER;

    return $definitions;
  }

  /**
   * Builds the event data array exposed as [event:*] tokens in ECA rules.
   *
   * TraceRecordedEvent provides: route, method, status_code, duration_ms,
   * is_ajax.
   *
   * TracesDeletedEvent provides: deleted_count.
   *
   * All values are cast to string because ECA's token replacement layer
   * expects scalar string values.
   */
  #[Token(
    name: 'event',
    description: 'The Native Observability event.',
    properties: [
      new Token(name: 'machine_name', description: 'The machine name of the ECA event plugin.'),
      new Token(
        name: 'route',
        description: 'The Drupal route machine name of the request (e.g. node.view).',
        classes: [TraceRecordedEvent::class],
      ),
      new Token(
        name: 'method',
        description: 'The HTTP request method in upper case (GET, POST, …).',
        classes: [TraceRecordedEvent::class],
      ),
      new Token(
        name: 'status_code',
        description: 'The HTTP response status code (e.g. 200, 404, 500).',
        classes: [TraceRecordedEvent::class],
      ),
      new Token(
        name: 'duration_ms',
        description: 'The request duration in milliseconds.',
        classes: [TraceRecordedEvent::class],
      ),
      new Token(
        name: 'is_ajax',
        description: 'Whether the request was an AJAX request: "1" for yes, "0" for no.',
        classes: [TraceRecordedEvent::class],
      ),
      new Token(
        name: 'deleted_count',
        description: 'The number of trace records that were deleted.',
        classes: [TracesDeletedEvent::class],
      ),
    ],
  )]
  protected function buildEventData(): array {
    $data = parent::buildEventData();

    if ($this->event instanceof TraceRecordedEvent) {
      $trace = $this->event->getTrace();

      $data['route'] = (string) ($trace['route_name'] ?? '');
      $data['method'] = strtoupper((string) ($trace['method'] ?? ''));
      $data['status_code'] = (string) ($trace['status_code'] ?? '');
      $data['duration_ms'] = (string) ((int) ($trace['duration_ms'] ?? 0));
      $data['is_ajax'] = (string) ((int) ($trace['is_ajax'] ?? 0));
    }

    if ($this->event instanceof TracesDeletedEvent) {
      $data['deleted_count'] = (string) $this->event->getDeletedCount();
    }

    return $data;
  }

}
