<?php

declare(strict_types=1);

namespace Drupal\native_observability_cache_observer\EventSubscriber;

use Drupal\Core\Cache\CacheableResponseInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\native_observability\EventSubscriber\TraceSubscriber;
use Drupal\native_observability\Service\RequestLogExclusionMatcher;
use Drupal\native_observability_cache_observer\Service\CacheObserverStorage;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;

/**
 * Observes response-level Drupal cache information.
 */
final class CacheResponseObserverSubscriber implements EventSubscriberInterface {

  /**
   * Creates a new response observer subscriber instance.
   */
  public function __construct(
    private readonly CacheObserverStorage $storage,
    #[Autowire(service: 'config.factory')]
    private readonly ConfigFactoryInterface $configFactory,
    #[Autowire(service: 'logger.channel.native_observability')]
    private readonly LoggerInterface $logger,
  ) {
  }

  /**
   * Gets the subscribed events.
   */
  public static function getSubscribedEvents(): array {
    return [
      KernelEvents::RESPONSE => ['onResponse', -90],
    ];
  }

  /**
   * Captures cache-related data from the response event.
   */
  public function onResponse(ResponseEvent $event): void {
    if (!$event->isMainRequest()) {
      return;
    }

    $config = $this->configFactory->get('native_observability_cache_observer.settings');
    if (!(bool) $config->get('capture_enabled')) {
      return;
    }

    $request = $event->getRequest();
    $response = $event->getResponse();

    if (RequestLogExclusionMatcher::isRequestExcluded($request)) {
      return;
    }

    try {
      $requestId = $request->attributes->get(TraceSubscriber::ATTR_REQUEST_ID);
      $baseRecord = [
        'request_id' => is_scalar($requestId) ? (string) $requestId : NULL,
        'method' => $request->getMethod(),
        'path' => $request->getPathInfo(),
        'route_name' => (string) ($request->attributes->get('_route') ?? ''),
      ];

      if ((bool) $config->get('capture_response_headers')) {
        $this->captureHeaderEvent('page_cache', (string) $response->headers->get('X-Drupal-Cache', ''), $baseRecord);
        $this->captureHeaderEvent('dynamic_page_cache', (string) $response->headers->get('X-Drupal-Dynamic-Cache', ''), $baseRecord);
      }

      if ((bool) $config->get('capture_response_cacheability') && $response instanceof CacheableResponseInterface) {
        $metadata = $response->getCacheableMetadata();
        $tags = array_values($metadata->getCacheTags());
        $contexts = array_values($metadata->getCacheContexts());
        $maxAge = $metadata->getCacheMaxAge();

        $this->storage->insert($baseRecord + [
          'event_type' => 'cacheability',
          'cache_layer' => 'response_cacheability',
          'status' => $this->deriveCacheabilityStatus($tags, $contexts, $maxAge),
          'tag_count' => count($tags),
          'context_count' => count($contexts),
          'max_age' => $maxAge,
          'payload' => [
            'tags' => $tags,
            'contexts' => $contexts,
            'max_age' => $maxAge,
          ],
        ]);
      }
    }
    catch (\Throwable $throwable) {
      $this->logger->warning('Cache observer could not record response cache metadata: @message', [
        '@message' => $throwable->getMessage(),
      ]);
    }
  }

  /**
   * Persists a cache-header observation event.
   *
   * @param string $layer
   *   The cache layer name.
   * @param string $headerValue
   *   The raw response header value.
   * @param array<string, mixed> $baseRecord
   *   The base event payload.
   */
  private function captureHeaderEvent(string $layer, string $headerValue, array $baseRecord): void {
    $normalized = strtoupper(trim($headerValue));
    if ($normalized === '') {
      return;
    }

    $this->storage->insert($baseRecord + [
      'event_type' => 'response_cache',
      'cache_layer' => $layer,
      'status' => $normalized,
      'tag_count' => 0,
      'context_count' => 0,
      'max_age' => NULL,
      'payload' => [
        'header_value' => $normalized,
      ],
    ]);
  }

  /**
   * Derives the normalized cacheability status label.
   *
   * @param array<int, string> $tags
   *   Cache tags associated with the response.
   * @param array<int, string> $contexts
   *   Cache contexts associated with the response.
   * @param int $maxAge
   *   The cache max-age value.
   *
   * @return string
   *   The normalized cacheability status.
   */
  private function deriveCacheabilityStatus(array $tags, array $contexts, int $maxAge): string {
    if ($maxAge === 0) {
      return 'UNCACHEABLE';
    }

    if ($maxAge > 0 || $maxAge === -1 || $tags || $contexts) {
      return 'CACHEABLE';
    }

    return 'UNKNOWN';
  }

}
