<?php

declare(strict_types=1);

namespace Drupal\native_observability\Service;

use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\native_observability\Event\NativeObservabilityEvents;
use Drupal\native_observability\Event\TraceRecordedEvent;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;

/**
 * Database-based trace storage implementation.
 *
 * Phase 1 stores traces in a dedicated table:
 * - native_observability_trace.
 *
 * Notes:
 * - This class intentionally performs straightforward SQL operations.
 * - We avoid Entity API to keep storage light and fast.
 * - We keep queries safe by using Drupal's database API placeholders.
 */
final class DatabaseTraceStorage implements TraceStorageInterface {

  /**
   * The DB table name for traces.
   */
  private const TABLE = 'native_observability_trace';

  /**
   * Drupal database connection.
   */
  private Connection $database;

  /**
   * The event dispatcher.
   */
  protected EventDispatcherInterface $eventDispatcher;

  /**
   * Constructs the storage.
   *
   * @param \Drupal\Core\Database\Connection $database
   *   Drupal database connection.
   * @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
   *   Event dispatcher service.
   */
  public function __construct(Connection $database, EventDispatcherInterface $event_dispatcher) {
    $this->database = $database;
    $this->eventDispatcher = $event_dispatcher;
  }

  /**
   * {@inheritdoc}
   */
  public function insert(array $trace): int {
    // Make sure optional keys exist (best-effort normalization).
    // The subscriber must remain fail-safe. Missing non-critical keys must
    // never break trace logging.
    $trace += [
      'route_name' => NULL,
      'status_code' => NULL,
      'is_ajax' => 0,
      'duration_ms' => NULL,
      'ended_at' => NULL,
      'uid' => NULL,
      'client_ip' => NULL,
      'payload' => NULL,
    ];

    $fields = [
      'request_id' => (string) ($trace['request_id'] ?? ''),
      'method' => (string) ($trace['method'] ?? ''),
      'path' => (string) ($trace['path'] ?? ''),
      'route_name' => $trace['route_name'],
      'status_code' => $trace['status_code'],
      'is_ajax' => (int) ($trace['is_ajax'] ?? 0),
      'duration_ms' => $trace['duration_ms'],
      'started_at' => (int) ($trace['started_at'] ?? time()),
      'ended_at' => $trace['ended_at'],
      'uid' => $trace['uid'],
      'client_ip' => $trace['client_ip'],
      'payload' => $trace['payload'],
    ];

    $id = (int) $this->database->insert(self::TABLE)
      ->fields($fields)
      ->execute();

    // Dispatch event after trace has been persisted.
    $this->eventDispatcher->dispatch(
      new TraceRecordedEvent([
        'id' => $id,
        'request_id' => $fields['request_id'] ?? NULL,
        'method' => $fields['method'] ?? NULL,
        'path' => $fields['path'] ?? NULL,
        'route_name' => $fields['route_name'] ?? NULL,
        'status_code' => $fields['status_code'] ?? NULL,
        'is_ajax' => $fields['is_ajax'] ?? NULL,
        'duration_ms' => $fields['duration_ms'] ?? NULL,
        'started_at' => $fields['started_at'] ?? NULL,
        'ended_at' => $fields['ended_at'] ?? NULL,
      ]),
      NativeObservabilityEvents::TRACE_RECORDED
    );

    return $id;
  }

  /**
   * {@inheritdoc}
   */
  public function load(int $id): ?array {
    $row = $this->database->select(self::TABLE, 't')
      ->fields('t')
      ->condition('id', $id)
      ->execute()
      ->fetchAssoc();

    return $row ?: NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function search(array $filters, int $limit = 50, int $offset = 0): array {
    $query = $this->baseSelect()
      ->range($offset, $limit);

    $this->applyFilters($query, $filters);

    // Sorting uses an allowlist to avoid invalid fields and to keep behavior
    // predictable.
    $allowed_sort = [
      'id',
      'started_at',
      'status_code',
      'duration_ms',
    ];

    $sort = (string) ($filters['sort'] ?? 'started_at');
    if (!in_array($sort, $allowed_sort, TRUE)) {
      $sort = 'started_at';
    }

    $direction = strtoupper((string) ($filters['direction'] ?? 'DESC'));
    if (!in_array($direction, ['ASC', 'DESC'], TRUE)) {
      $direction = 'DESC';
    }

    $query->orderBy($sort, $direction);

    return $query->execute()->fetchAllAssoc('id') ?: [];
  }

  /**
   * {@inheritdoc}
   */
  public function count(array $filters): int {
    $query = $this->database->select(self::TABLE, 't');
    $query->addExpression('COUNT(t.id)', 'count');

    $this->applyFilters($query, $filters);

    // fetchField() returns the first column, i.e. our COUNT expression.
    return (int) $query->execute()->fetchField();
  }

  /**
   * Builds the base select query for traces.
   *
   * @return \Drupal\Core\Database\Query\SelectInterface
   *   Select query with standard fields.
   */
  private function baseSelect(): SelectInterface {
    return $this->database->select(self::TABLE, 't')
      ->fields('t', [
        'id',
        'request_id',
        'method',
        'path',
        'route_name',
        'status_code',
        'is_ajax',
        'duration_ms',
        'started_at',
        'ended_at',
        'uid',
        'client_ip',
      ]);
  }

  /**
   * Applies supported filters to a select query.
   *
   * @param \Drupal\Core\Database\Query\SelectInterface $query
   *   The query to modify.
   * @param array $filters
   *   Filters array.
   */
  private function applyFilters(SelectInterface $query, array $filters): void {
    // Status code filter:
    // - supports exact match (e.g. 404)
    // - supports groups (2xx, 3xx, 4xx, 5xx)
    if (isset($filters['status_code']) && $filters['status_code'] !== '') {
      $value = (string) $filters['status_code'];

      if (preg_match('/^[2-5]xx$/', $value)) {
        $min = ((int) $value[0]) * 100;
        $max = $min + 99;
        $query->condition('status_code', $min, '>=');
        $query->condition('status_code', $max, '<=');
      }
      else {
        $query->condition('status_code', (int) $value);
      }
    }

    // AJAX filter.
    if (isset($filters['is_ajax']) && $filters['is_ajax'] !== '') {
      $query->condition('is_ajax', (int) $filters['is_ajax']);
    }

    // HTTP method exact match (e.g. GET, POST).
    if (!empty($filters['method'])) {
      $query->condition('method', (string) $filters['method']);
    }

    // Backward compatibility:
    // - "path" was an initial filter.
    // - "search" is the new global search term.
    if (empty($filters['search']) && !empty($filters['path'])) {
      $filters['search'] = $filters['path'];
    }

    // Global search across common columns.
    // This keeps the UI simple: one input can search path, request
    // ID, method, and route name.
    if (!empty($filters['search'])) {
      $term = (string) $filters['search'];
      $term = $this->database->escapeLike($term);

      $group = $query->orConditionGroup()
        ->condition('path', '%' . $term . '%', 'LIKE')
        ->condition('request_id', '%' . $term . '%', 'LIKE')
        ->condition('method', '%' . $term . '%', 'LIKE')
        ->condition('route_name', '%' . $term . '%', 'LIKE');

      $query->condition($group);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function deleteAll(): int {
    // Use DELETE (not TRUNCATE) to stay portable and avoid privilege issues.
    return (int) $this->database->delete(self::TABLE)->execute();
  }

  /**
   * {@inheritdoc}
   */
  public function loadByRequestId(string $request_id): ?array {
    $row = $this->database->select(self::TABLE, 't')
      ->fields('t')
      ->condition('request_id', $request_id)
      ->range(0, 1)
      ->execute()
      ->fetchAssoc();

    return $row ?: NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function loadLatestByRequestId(string $request_id): ?array {
    $row = $this->database
      ->select(self::TABLE, 't')
      ->fields('t')
      ->condition('request_id', $request_id)
      ->orderBy('id', 'DESC')
      ->range(0, 1)
      ->execute()
      ->fetchAssoc();

    return $row ?: NULL;
  }

}
