<?php

declare(strict_types=1);

namespace Drupal\native_observability_cache_observer\Service;

use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\ContainerInterface;

/**
 * Builds and caches a compact runtime environment snapshot.
 *
 * This service is designed for observability workloads and follows a
 * low-overhead strategy:
 * - Static in-request memoization avoids repeated work in the same request.
 * - Persistent Drupal cache avoids re-detecting the environment on every hit.
 * - The resulting snapshot can be embedded into stored observability records.
 *
 * The snapshot is intentionally compact but descriptive enough to explain
 * cache backend changes, infrastructure drift, and runtime differences
 * across environments.
 */
final class CacheObserverEnvironmentSnapshotManager {

  /**
   * Snapshot cache ID.
   */
  private const SNAPSHOT_CACHE_ID = 'native_observability_cache_observer.environment_snapshot';

  /**
   * Database info cache ID.
   */
  private const DATABASE_INFO_CACHE_ID = 'native_observability_cache_observer.database_info';

  /**
   * Snapshot cache TTL in seconds.
   *
   * This can remain relatively short because the collection itself is cheap.
   */
  private const SNAPSHOT_TTL = 300;

  /**
   * Database info cache TTL in seconds.
   *
   * Database version changes are extremely rare during runtime.
   */
  private const DATABASE_INFO_TTL = 21600;

  /**
   * Common cache bins to inspect.
   */
  private const CACHE_BINS = [
    'default',
    'bootstrap',
    'config',
    'data',
    'discovery',
    'render',
    'dynamic_page_cache',
    'page',
    'entity',
  ];

  /**
   * In-request static memoized snapshot.
   */
  private ?array $runtimeSnapshot = NULL;

  /**
   * Creates a new environment snapshot manager instance.
   */
  public function __construct(
    #[Autowire(service: 'cache.default')]
    private readonly CacheBackendInterface $cacheBackend,
    private readonly ModuleHandlerInterface $moduleHandler,
    private readonly Connection $database,
    private readonly ContainerInterface $container,
    #[Autowire(service: 'config.factory')]
    private readonly ConfigFactoryInterface $configFactory,
  ) {
  }

  /**
   * Returns the current environment snapshot with layered caching.
   *
   * Use this method from record builders. It is safe to call multiple times
   * in the same request because the first result is memoized in memory.
   */
  public function getSnapshot(bool $forceRefresh = FALSE): array {
    if (!$forceRefresh && $this->runtimeSnapshot !== NULL) {
      return $this->runtimeSnapshot;
    }

    if (!$forceRefresh) {
      $cached = $this->cacheBackend->get(self::SNAPSHOT_CACHE_ID);
      if ($cached !== FALSE && is_array($cached->data)) {
        $this->runtimeSnapshot = $cached->data;
        return $this->runtimeSnapshot;
      }
    }

    $snapshot = $this->buildSnapshot();
    $snapshot['fingerprint'] = $this->buildFingerprint($snapshot);
    $snapshot['captured_at'] = gmdate('c');

    $this->runtimeSnapshot = $snapshot;

    $this->cacheBackend->set(
      self::SNAPSHOT_CACHE_ID,
      $snapshot,
      time() + self::SNAPSHOT_TTL
    );

    return $this->runtimeSnapshot;
  }

  /**
   * Builds the full environment snapshot.
   */
  private function buildSnapshot(): array {
    return [
      'php_version' => PHP_VERSION,
      'drupal_version' => \Drupal::VERSION,
      'database' => $this->getDatabaseInfo(),
      'cache' => [
        'bins' => $this->getCacheBinSnapshot(),
        'modules' => $this->getCacheModuleSnapshot(),
        'page_cache' => $this->getPageCacheSettingsSnapshot(),
      ],
    ];
  }

  /**
   * Returns a compact database snapshot.
   *
   * The version query is cached separately because it is the only part that
   * may trigger an actual database roundtrip.
   */
  private function getDatabaseInfo(): array {
    $cached = $this->cacheBackend->get(self::DATABASE_INFO_CACHE_ID);
    if ($cached !== FALSE && is_array($cached->data)) {
      return $cached->data;
    }

    $info = [
      'driver' => $this->database->driver(),
      'version' => 'unknown',
    ];

    try {
      $version = $this->database
        ->query('SELECT VERSION()')
        ->fetchField();

      if (is_scalar($version) && $version !== '') {
        $info['version'] = (string) $version;
      }
    }
    catch (\Throwable) {
      // Keep a safe fallback. Observability must never break runtime flow.
    }

    $this->cacheBackend->set(
      self::DATABASE_INFO_CACHE_ID,
      $info,
      time() + self::DATABASE_INFO_TTL
    );

    return $info;
  }

  /**
   * Returns the snapshot for common Drupal cache bins.
   */
  private function getCacheBinSnapshot(): array {
    $result = [];

    foreach (self::CACHE_BINS as $bin) {
      $serviceId = 'cache.' . $bin;

      if (!$this->container->has($serviceId)) {
        continue;
      }

      try {
        $backend = $this->container->get($serviceId);
        $class = get_class($backend);

        $result[$bin] = [
          'type' => $this->normalizeCacheBackendType($class),
          'class' => $class,
        ];
      }
      catch (\Throwable) {
        $result[$bin] = [
          'type' => 'unresolved',
          'class' => 'unresolved',
        ];
      }
    }

    return $result;
  }

  /**
   * Returns installed cache-related modules.
   */
  private function getCacheModuleSnapshot(): array {
    return [
      'redis' => $this->moduleHandler->moduleExists('redis'),
      'memcache' => $this->moduleHandler->moduleExists('memcache'),
      'apcu' => $this->moduleHandler->moduleExists('apcu'),
    ];
  }

  /**
   * Returns a minimal page cache settings snapshot.
   */
  private function getPageCacheSettingsSnapshot(): array {
    $systemPerformance = $this->configFactory->get('system.performance');

    return [
      'page_cache_maximum_age' => (int) ($systemPerformance->get('cache.page.max_age') ?? 0),
    ];
  }

  /**
   * Normalizes a backend class name into a human-readable cache type.
   */
  private function normalizeCacheBackendType(string $class): string {
    $normalized = strtolower($class);

    if (str_contains($normalized, 'chainedfast')) {
      return 'chainedfast';
    }

    if (str_contains($normalized, 'redis')) {
      return 'redis';
    }

    if (str_contains($normalized, 'memcache')) {
      return 'memcache';
    }

    if (str_contains($normalized, 'apcu')) {
      return 'apcu';
    }

    if (str_contains($normalized, 'database')) {
      return 'database';
    }

    if (str_contains($normalized, 'file')) {
      return 'file';
    }

    if (str_contains($normalized, 'null')) {
      return 'null';
    }

    return 'custom';
  }

  /**
   * Builds a stable fingerprint for the snapshot.
   *
   * This can be used to compare records and detect infrastructure drift.
   */
  private function buildFingerprint(array $snapshot): string {
    return hash('sha256', json_encode($snapshot, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE) ?: '');
  }

}
