<?php

declare(strict_types=1);

namespace Drupal\native_observability\Service;

use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
use Drupal\Core\Database\Connection;

/**
 * Stores and loads request-log exclusion rules.
 */
final class RequestLogExclusionRepository {

  public const TABLE = 'native_observability_request_exclusion_rule';
  public const CACHE_ID = 'native_observability.request_log_exclusion_rules';
  public const CACHE_TAG = 'native_observability:request_log_exclusion_rules';

  /**
   * In-memory copy of normalized rules for the current lifecycle.
   *
   * @var array<int, array<string, mixed>>|null
   */
  private ?array $normalizedRules = NULL;

  public function __construct(
    private readonly Connection $database,
    private readonly CacheBackendInterface $cache,
    private readonly CacheTagsInvalidatorInterface $cacheTagsInvalidator,
    private readonly RequestLogExclusionRuleNormalizer $normalizer,
  ) {
  }

  /**
   * Loads all rules for admin listing.
   *
   * @return array<int, array<string, mixed>>
   *   Stored rules ordered by newest first.
   */
  public function loadAll(): array {
    $result = $this->database->select(self::TABLE, 'r')
      ->fields('r')
      ->orderBy('id', 'DESC')
      ->execute()
      ->fetchAllAssoc('id', \PDO::FETCH_ASSOC);

    return $result ? array_values($result) : [];
  }

  /**
   * Loads one rule by ID.
   *
   * @param int $id
   *   Rule ID.
   *
   * @return array<string, mixed>|null
   *   Stored rule or NULL.
   */
  public function load(int $id): ?array {
    $row = $this->database->select(self::TABLE, 'r')
      ->fields('r')
      ->condition('id', $id)
      ->execute()
      ->fetchAssoc();

    return $row ?: NULL;
  }

  /**
   * Returns runtime-normalized enabled rules.
   *
   * Uses persistent cache first and memoizes the result in memory so normal
   * request processing never performs repeated DB lookups.
   *
   * @return array<int, array<string, mixed>>
   *   Normalized enabled rules.
   */
  public function loadNormalizedEnabledRules(): array {
    if ($this->normalizedRules !== NULL) {
      return $this->normalizedRules;
    }

    $cached = $this->cache->get(self::CACHE_ID);
    if ($cached && is_array($cached->data)) {
      $this->normalizedRules = $cached->data;
      return $this->normalizedRules;
    }

    $rules = $this->database->select(self::TABLE, 'r')
      ->fields('r')
      ->condition('enabled', 1)
      ->orderBy('id', 'ASC')
      ->execute()
      ->fetchAll(\PDO::FETCH_ASSOC) ?: [];

    $this->normalizedRules = $this->normalizer->normalizeRules($rules);
    $this->cache->set(self::CACHE_ID, $this->normalizedRules, Cache::PERMANENT, [self::CACHE_TAG]);

    return $this->normalizedRules;
  }

  /**
   * Saves a rule and invalidates runtime cache.
   *
   * @param array<string, mixed> $values
   *   Rule values.
   *
   * @return int
   *   Saved rule ID.
   */
  public function save(array $values): int {
    $values += [
      'enabled' => 1,
      'method' => 'ANY',
      'path_match_type' => 'exact',
      'query_conditions' => '[]',
      'notes' => '',
    ];

    $fields = [
      'label' => (string) ($values['label'] ?? ''),
      'enabled' => !empty($values['enabled']) ? 1 : 0,
      'method' => strtoupper((string) ($values['method'] ?? 'ANY')),
      'path_match_type' => (string) ($values['path_match_type'] ?? 'exact'),
      'path_pattern' => (string) ($values['path_pattern'] ?? ''),
      'query_conditions' => (string) ($values['query_conditions'] ?? '[]'),
      'notes' => (string) ($values['notes'] ?? ''),
      'updated_at' => time(),
    ];

    $id = (int) ($values['id'] ?? 0);
    if ($id > 0) {
      $this->database->update(self::TABLE)
        ->fields($fields)
        ->condition('id', $id)
        ->execute();
    }
    else {
      $fields['created_at'] = time();
      $id = (int) $this->database->insert(self::TABLE)
        ->fields($fields)
        ->execute();
    }

    $this->invalidateRuntimeCache();
    return $id;
  }

  /**
   * Deletes a rule and invalidates runtime cache.
   *
   * @param int $id
   *   Rule ID.
   */
  public function delete(int $id): void {
    $this->database->delete(self::TABLE)
      ->condition('id', $id)
      ->execute();

    $this->invalidateRuntimeCache();
  }

  /**
   * Finds one rule matching a canonical method/path/query definition.
   *
   * @param string $method
   *   HTTP method.
   * @param string $pathMatchType
   *   Path match type.
   * @param string $pathPattern
   *   Path pattern.
   * @param string $queryConditions
   *   Encoded query conditions.
   *
   * @return array<string, mixed>|null
   *   Matching rule or NULL.
   */
  public function findByCanonicalDefinition(
    string $method,
    string $pathMatchType,
    string $pathPattern,
    string $queryConditions = '[]',
  ): ?array {
    $row = $this->database->select(self::TABLE, 'r')
      ->fields('r')
      ->condition('method', strtoupper($method))
      ->condition('path_match_type', $pathMatchType)
      ->condition('path_pattern', $pathPattern)
      ->condition('query_conditions', $queryConditions)
      ->range(0, 1)
      ->execute()
      ->fetchAssoc();

    return $row ?: NULL;
  }

  /**
   * Invalidates persistent and in-memory rule cache.
   */
  public function invalidateRuntimeCache(): void {
    $this->normalizedRules = NULL;
    $this->cacheTagsInvalidator->invalidateTags([self::CACHE_TAG]);
  }

}
