<?php

declare(strict_types=1);

namespace Drupal\native_observability_cache_observer\Service;

use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Query\SelectInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
 * Provides raw storage and query helpers for cache observer events.
 */
final class CacheObserverStorage {

  /**
   * Storage table name.
   */
  private const TABLE = 'native_observability_cache_event';

  /**
   * Creates a new cache observer storage instance.
   */
  public function __construct(
    #[Autowire(service: 'database')]
    private readonly Connection $database,
  ) {
  }

  /**
   * Indicates whether the storage table exists.
   */
  public function tableExists(): bool {
    return $this->database->schema()->tableExists(self::TABLE);
  }

  /**
   * Persists a cache observer event.
   */
  public function insert(array $record): void {
    if (!$this->tableExists()) {
      return;
    }

    $this->database->insert(self::TABLE)
      ->fields([
        'request_id' => $record['request_id'] ?? NULL,
        'event_type' => $record['event_type'] ?? 'unknown',
        'cache_layer' => $record['cache_layer'] ?? NULL,
        'status' => $record['status'] ?? NULL,
        'method' => $record['method'] ?? NULL,
        'path' => $record['path'] ?? NULL,
        'route_name' => $record['route_name'] ?? NULL,
        'tag_count' => (int) ($record['tag_count'] ?? 0),
        'context_count' => (int) ($record['context_count'] ?? 0),
        'max_age' => $record['max_age'] ?? NULL,
        'created_at' => (int) ($record['created_at'] ?? time()),
        'payload' => isset($record['payload'])
          ? json_encode($record['payload'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE)
          : NULL,
      ])
      ->execute();
  }

  /**
   * Counts raw events matching the selected filters.
   */
  public function count(array $filters = []): int {
    if (!$this->tableExists()) {
      return 0;
    }

    $query = $this->database->select(self::TABLE, 'e');
    $query->addExpression('COUNT(e.id)', 'count');
    $this->applyFilters($query, $filters);

    return (int) $query->execute()->fetchField();
  }

  /**
   * Searches raw events matching the selected filters.
   */
  public function search(array $filters = [], int $limit = 50, int $offset = 0): array {
    if (!$this->tableExists()) {
      return [];
    }

    $allowedSorts = [
      'id',
      'created_at',
      'event_type',
      'cache_layer',
      'status',
      'method',
      'path',
      'route_name',
      'tag_count',
      'context_count',
      'max_age',
    ];

    $sort = (string) ($filters['sort'] ?? 'created_at');
    $sort = in_array($sort, $allowedSorts, TRUE) ? $sort : 'created_at';

    $direction = strtoupper((string) ($filters['direction'] ?? 'DESC'));
    $direction = in_array($direction, ['ASC', 'DESC'], TRUE) ? $direction : 'DESC';

    $query = $this->database->select(self::TABLE, 'e')
      ->fields('e')
      ->orderBy($sort, $direction)
      ->range(max(0, $offset), max(1, $limit));

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

    $rows = [];
    foreach ($query->execute()->fetchAllAssoc('id') as $row) {
      $decodedPayload = [];
      if (!empty($row->payload)) {
        $decoded = json_decode((string) $row->payload, TRUE);
        $decodedPayload = is_array($decoded) ? $decoded : [];
      }

      $rows[] = [
        'id' => (int) $row->id,
        'request_id' => (string) ($row->request_id ?? ''),
        'event_type' => (string) ($row->event_type ?? ''),
        'cache_layer' => (string) ($row->cache_layer ?? ''),
        'status' => (string) ($row->status ?? ''),
        'method' => (string) ($row->method ?? ''),
        'path' => (string) ($row->path ?? ''),
        'route_name' => (string) ($row->route_name ?? ''),
        'tag_count' => (int) $row->tag_count,
        'context_count' => (int) $row->context_count,
        'max_age' => $row->max_age === NULL ? '' : (string) $row->max_age,
        'created_at' => (int) $row->created_at,
        'payload' => $decodedPayload,
      ];
    }

    return $rows;
  }

  /**
   * Loads a single raw event by ID.
   */
  public function load(int $id): ?array {
    if (!$this->tableExists()) {
      return NULL;
    }

    $row = $this->database->select(self::TABLE, 'e')
      ->fields('e')
      ->condition('id', $id)
      ->execute()
      ->fetchAssoc();

    if (!$row) {
      return NULL;
    }

    $payload = [];
    if (!empty($row['payload'])) {
      $decoded = json_decode((string) $row['payload'], TRUE);
      $payload = is_array($decoded) ? $decoded : [];
    }

    return [
      'id' => (int) $row['id'],
      'request_id' => (string) ($row['request_id'] ?? ''),
      'event_type' => (string) ($row['event_type'] ?? ''),
      'cache_layer' => (string) ($row['cache_layer'] ?? ''),
      'status' => (string) ($row['status'] ?? ''),
      'method' => (string) ($row['method'] ?? ''),
      'path' => (string) ($row['path'] ?? ''),
      'route_name' => (string) ($row['route_name'] ?? ''),
      'tag_count' => (int) $row['tag_count'],
      'context_count' => (int) $row['context_count'],
      'max_age' => $row['max_age'],
      'created_at' => (int) $row['created_at'],
      'payload' => $payload,
    ];
  }

  /**
   * Returns summary counters for the selected time window.
   */
  public function getSummary(int $hours = 24): array {
    if (!$this->tableExists()) {
      return $this->emptySummary();
    }

    $since = time() - max(1, $hours) * 3600;
    $query = $this->database->select(self::TABLE, 'e')
      ->fields('e', ['event_type', 'cache_layer', 'status', 'tag_count'])
      ->condition('created_at', $since, '>=');

    $summary = $this->emptySummary();

    foreach ($query->execute() as $row) {
      $summary['total_events']++;

      if ($row->event_type === 'tag_invalidation') {
        $summary['invalidation_events']++;
        $summary['invalidated_tags'] += (int) $row->tag_count;
      }

      if ($row->event_type === 'response_cache' && $row->cache_layer === 'page_cache') {
        if ($row->status === 'HIT') {
          $summary['page_cache_hits']++;
        }
        elseif ($row->status === 'MISS') {
          $summary['page_cache_misses']++;
        }
      }

      if ($row->event_type === 'response_cache' && $row->cache_layer === 'dynamic_page_cache') {
        if ($row->status === 'HIT') {
          $summary['dynamic_cache_hits']++;
        }
        elseif ($row->status === 'MISS') {
          $summary['dynamic_cache_misses']++;
        }
      }
    }

    return $summary;
  }

  /**
   * Returns recent observed cache events.
   */
  public function getRecentEvents(int $limit = 100): array {
    return $this->search([], $limit, 0);
  }

  /**
   * Deletes all stored events.
   */
  public function deleteAll(): int {
    if (!$this->tableExists()) {
      return 0;
    }

    return (int) $this->database->truncate(self::TABLE)->execute();
  }

  /**
   * Deletes events matching the selected filters.
   */
  public function deleteByFilters(array $filters = [], int $limit = 5000): int {
    if (!$this->tableExists()) {
      return 0;
    }

    $query = $this->database->select(self::TABLE, 'e')
      ->fields('e', ['id'])
      ->range(0, max(1, $limit));

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

    $ids = $query->execute()->fetchCol();
    if (!$ids) {
      return 0;
    }

    return (int) $this->database->delete(self::TABLE)
      ->condition('id', $ids, 'IN')
      ->execute();
  }

  /**
   * Deletes events older than the selected timestamp.
   */
  public function deleteOlderThan(int $threshold, int $limit): int {
    if (!$this->tableExists()) {
      return 0;
    }

    $ids = $this->database->select(self::TABLE, 'e')
      ->fields('e', ['id'])
      ->condition('created_at', $threshold, '<')
      ->orderBy('id', 'ASC')
      ->range(0, max(1, $limit))
      ->execute()
      ->fetchCol();

    if (!$ids) {
      return 0;
    }

    return (int) $this->database->delete(self::TABLE)
      ->condition('id', $ids, 'IN')
      ->execute();
  }

  /**
   * Applies raw filters to the selected query.
   */
  private function applyFilters(SelectInterface $query, array $filters): void {
    if (!empty($filters['search'])) {
      $group = $query->orConditionGroup()
        ->condition('request_id', '%' . $this->database->escapeLike((string) $filters['search']) . '%', 'LIKE')
        ->condition('path', '%' . $this->database->escapeLike((string) $filters['search']) . '%', 'LIKE')
        ->condition('route_name', '%' . $this->database->escapeLike((string) $filters['search']) . '%', 'LIKE')
        ->condition('status', '%' . $this->database->escapeLike((string) $filters['search']) . '%', 'LIKE')
        ->condition('cache_layer', '%' . $this->database->escapeLike((string) $filters['search']) . '%', 'LIKE')
        ->condition('event_type', '%' . $this->database->escapeLike((string) $filters['search']) . '%', 'LIKE');
      $query->condition($group);
    }

    if (!empty($filters['event_type'])) {
      $query->condition('event_type', (string) $filters['event_type']);
    }

    if (!empty($filters['cache_layer'])) {
      $query->condition('cache_layer', (string) $filters['cache_layer']);
    }

    if (!empty($filters['status'])) {
      $query->condition('status', (string) $filters['status']);
    }

    if (!empty($filters['method'])) {
      $query->condition('method', (string) $filters['method']);
    }

    if (!empty($filters['route_name'])) {
      $query->condition('route_name', '%' . $this->database->escapeLike((string) $filters['route_name']) . '%', 'LIKE');
    }

    if (!empty($filters['path'])) {
      $query->condition('path', '%' . $this->database->escapeLike((string) $filters['path']) . '%', 'LIKE');
    }

    if (!empty($filters['request_id'])) {
      $query->condition('request_id', '%' . $this->database->escapeLike((string) $filters['request_id']) . '%', 'LIKE');
    }

    if (($filters['has_request_id'] ?? '') === '1') {
      $query->isNotNull('request_id');
      $query->condition('request_id', '', '<>');
    }

    if (($filters['uncacheable_only'] ?? '') === '1') {
      $uncacheable = $query->orConditionGroup()
        ->condition('status', 'UNCACHEABLE')
        ->condition('max_age', 0);
      $query->condition($uncacheable);
    }

    if (($filters['created_from'] ?? '') !== '') {
      $query->condition('created_at', (int) $filters['created_from'], '>=');
    }

    if (($filters['created_to'] ?? '') !== '') {
      $query->condition('created_at', (int) $filters['created_to'], '<=');
    }
  }

  /**
   * Returns the empty summary structure.
   */
  private function emptySummary(): array {
    return [
      'total_events' => 0,
      'page_cache_hits' => 0,
      'page_cache_misses' => 0,
      'dynamic_cache_hits' => 0,
      'dynamic_cache_misses' => 0,
      'invalidation_events' => 0,
      'invalidated_tags' => 0,
    ];
  }

}
