<?php

namespace Drupal\api_monitor_client;

use Drupal\api_monitor_client\Alert\AlertManager;
use Drupal\api_monitor_client\Entity\ApiMonitorProviderInterface;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\State\StateInterface;
use GuzzleHttp\ClientInterface;
use Drupal\Core\Database\Connection;
use Drupal\Core\Config\ConfigFactoryInterface;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\Utils;
use GuzzleHttp\TransferStats;
use Psr\Log\LoggerInterface;

/**
 * Class ApiSnifferService for manage API Sniffer.
 */
class ApiSnifferService {

  /**
   * The configuration object.
   */
  private ImmutableConfig $config;

  /**
   * Collection delle API da monitorare.
   */
  protected array $discoveryCollection = [];

  /**
   * Token OAuth2.
   */
  protected string $token = '';

  /**
   * ApiSnifferService constructor.
   *
   * @param \GuzzleHttp\ClientInterface $httpClient
   *   The http client.
   * @param \Drupal\Core\Database\Connection $database
   *   The database connection.
   * @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
   *   The config factory.
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
   * @param \Drupal\Core\Cache\CacheBackendInterface $cache
   *   The cache backend.
   * @param \Drupal\api_monitor_client\ApiAuthInterface $apiAuth
   *   The API authentication service.
   * @param \Drupal\api_monitor_client\ApiMonitorProbeManager $probeManager
   *   The API monitor probe manager.
   * @param \Drupal\api_monitor_client\Alert\AlertManager $alertManager
   *   The alert manager.
   * @param \Drupal\Core\State\StateInterface $state
   *   The state service.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $moduleHandler
   *   The module handler.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
   *   The entity type manager.
   */
  public function __construct(
    private readonly ClientInterface $httpClient,
    private readonly Connection $database,
    private readonly ConfigFactoryInterface $configFactory,
    private readonly LoggerInterface $logger,
    private readonly CacheBackendInterface $cache,
    protected ApiAuthInterface $apiAuth,
    private readonly ApiMonitorProbeManager $probeManager,
    private readonly AlertManager $alertManager,
    private readonly StateInterface $state,
    private readonly ModuleHandlerInterface $moduleHandler,
    private readonly EntityTypeManagerInterface $entityTypeManager,
  ) {
    $this->config = $this->configFactory->get('api_monitor_client.settings');
  }

  /**
   * Retrieves the OAuth2 access token for a specific provider.
   *
   * @param \Drupal\api_monitor_client\Entity\ApiMonitorProviderInterface $provider
   *   The provider entity.
   *
   * @return string|null
   *   The access token, or NULL if an error occurs.
   */
  protected function getAccessToken(ApiMonitorProviderInterface $provider) {
    $cid = 'api_monitor_client:oauth_token:' . $provider->id();
    if ($cache = $this->cache->get($cid)) {
      return $cache->data;
    }

    $baseUrl = $provider->getProviderUrl();
    $token_endpoint = $provider->getTokenEndpoint() ?: '/oauth/token';
    $endpoint_url = $baseUrl . $token_endpoint;

    try {
      $response = $this->httpClient->post($endpoint_url, [
        'form_params' => [
          'grant_type' => 'client_credentials',
          'client_id' => $provider->getClientId(),
          'client_secret' => $provider->getClientSecret(),
          'scope' => $provider->getScope(),
        ],
      ]);

      $data = json_decode($response->getBody()->getContents(), TRUE);
      $token = $data['access_token'];
      $expires_in = $data['expires_in'];

      $this->cache->set($cid, $token, time() + $expires_in - 10);

      return $token;
    }
    catch (RequestException $e) {
      $response = $e->getResponse();
      $message = $response ? $response->getBody()->getContents() : $e->getMessage();

      $this->logger->error('Impossibile ottenere OAuth Token per ' . $provider->label() . ': ' . $message);
      return NULL;
    }
    catch (\Exception $e) {
      $this->logger->error('Errore generico durante l\'ottenimento del token OAuth: ' . $e->getMessage());
      return NULL;
    }
  }

  /**
   * Prepara i parametri della richiesta (URL, opzioni, metodo, probe).
   *
   * @param array $api
   *   L'array dell'API dal discovery.
   * @param \Drupal\api_monitor_client\Entity\ApiMonitorProviderInterface $provider
   *   The provider entity.
   *
   * @return array|null
   *   Dati della richiesta o NULL in caso di errore di validazione.
   */
  protected function prepareRequest(array $api, ApiMonitorProviderInterface $provider): ?array {
    $api_id = $api['id'];
    $baseUrl = $provider->getProviderUrl();
    $path = $api['path'] ?? '';

    // Security Check (SSRF) - allow absolute URLs only for manual endpoints
    if (str_contains($path, '://') || str_starts_with($path, '//')) {
      if (!($api['manual'] ?? FALSE)) {
        $this->logger->warning('SSRF attempt detected for API ID: @id', ['@id' => $api_id]);
        return NULL;
      }
    }

    // For manual endpoints with absolute URLs
    if (($api['manual'] ?? FALSE) && (str_starts_with($path, 'http://') || str_starts_with($path, 'https://'))) {
      $endpoint_url = $path;
    } else {
      if (!str_starts_with($path, '/')) {
        $path = '/' . $path;
      }
      $endpoint_url = $baseUrl . $path;
    }

    $method = $api['methods'][0] ?? 'GET';

    $request_options = [
      'timeout' => 30,
      'http_errors' => FALSE,
      'headers' => [
        'Accept' => 'application/json',
      ],
    ];

    // Authentication handling
    $auth_type = $api['auth_type'] ?? 'inherit';

    if ($auth_type === 'inherit') {
      if ($provider->isAuthEnabled()) {
        $token = $this->getAccessToken($provider);
        if (!empty($api['auth_required']) && !$token) {
          return NULL;
        }
        if ($token) {
          $request_options['headers']['Authorization'] = 'Bearer ' . $token;
        }
      }
    } elseif ($auth_type === 'oauth2') {
      $token = $this->getAccessToken($provider);
      if ($token) {
        $request_options['headers']['Authorization'] = 'Bearer ' . $token;
      }
    } elseif ($auth_type === 'bearer') {
      $token = $api['auth_credentials'] ?? '';
      if ($token) {
        $request_options['headers']['Authorization'] = 'Bearer ' . $token;
      }
    } elseif ($auth_type === 'api_key') {
      $api_key = $api['auth_credentials'] ?? '';
      $header_name = $api['auth_header_name'] ?? 'X-API-Key';
      if ($api_key && $header_name) {
        $request_options['headers'][$header_name] = $api_key;
      }
    }

    // Custom headers from manual endpoint
    if (!empty($api['headers'])) {
      $request_options['headers'] = array_merge(
        $request_options['headers'] ?? [],
        $api['headers']
      );
    }

    // Body for POST/PUT/PATCH
    if (!empty($api['body']) && in_array($method, ['POST', 'PUT', 'PATCH'])) {
      $request_options['body'] = $api['body'];
    }

    // Probe integration
    $probe_instance = NULL;
    if (!empty($api['probe_plugin'])) {
      $probes = $this->probeManager->getDefinitions();
      if (isset($probes[$api['probe_plugin']])) {
        $probe_instance = $this->probeManager->createInstance($api['probe_plugin']);
      }
    } else {
      // Legacy probe matching by endpoint_id
      $probes = $this->probeManager->getDefinitions();
      foreach ($probes as $plugin_id => $definition) {
        if ($definition['endpoint_id'] === $api_id) {
          $probe_instance = $this->probeManager->createInstance($plugin_id);
          if ($probe_instance) {
            // Route parameters {param}
            $params = $probe_instance->getRouteParameters() ?? [];
            foreach ($params as $key => $value) {
              $endpoint_url = str_replace('{' . $key . '}', (string) $value, $endpoint_url);
            }
            // Query string
            $query = $probe_instance->getQueryOptions() ?? [];
            if (!empty($query)) {
              $request_options['query'] = $query;
            }
            // Payload and method
            $request_options = array_merge_recursive($request_options, $probe_instance->getPayload());
            $method = $definition['method'];
          }
          break;
        }
      }
    }

    return [
      'method' => $method,
      'url' => $endpoint_url,
      'options' => $request_options,
      'probe' => $probe_instance,
    ];
  }

  /**
   * Esegue una chiamata sincrona a un endpoint API.
   *
   * @param array $api
   *   L'array dell'API.
   * @param \Drupal\api_monitor_client\Entity\ApiMonitorProviderInterface $provider
   *   The provider entity.
   *
   * @return \Psr\Http\Message\ResponseInterface|null
   *   La risposta o NULL.
   */
  public function call(array $api, ApiMonitorProviderInterface $provider) {
    $data = $this->prepareRequest($api, $provider);
    if (!$data) {
      return NULL;
    }

    try {
      return $this->httpClient->request($data['method'], $data['url'], $data['options']);
    }
    catch (\Exception $e) {
      $this->logger->error('Errore call per @id: @msg', ['@id' => $api['id'], '@msg' => $e->getMessage()]);
      return NULL;
    }
  }

  /**
   * Executes health and performance checks for configured APIs and probes.
   */
  public function runCheck() {
    /** @var \Drupal\api_monitor_client\Entity\ApiMonitorProviderInterface[] $providers */
    $providers = $this->entityTypeManager->getStorage('api_monitor_provider')->loadMultiple();

    foreach ($providers as $provider) {
      $apis = $this->getApiDiscovery($provider);
      if (empty($apis)) {
        continue;
      }

      $promises = $active_probes = $stats_data = [];

      foreach ($apis as $api) {
        $api_id = $api['id'];
        $req = $this->prepareRequest($api, $provider);
        if (!$req) {
          continue;
        }

        $options = $req['options'];
        $options['on_stats'] = function (TransferStats $stats) use (&$stats_data, $api_id) {
          $stats_data[$api_id] = $stats->getTransferTime() * 1000;
        };

        if ($req['probe']) {
          $active_probes[$api_id] = $req['probe'];
        }

        $promises[$api_id] = $this->httpClient->requestAsync($req['method'], $req['url'], $options);
      }

      $results = Utils::settle($promises)->wait();

      foreach ($results as $api_id => $result) {
        $api_read = array_column($apis, 'label', 'id');
        $response_time = $stats_data[$api_id] ?? 0;
        $this->moduleHandler->alter('api_monitor_report', $result, $api_id);

        $data = [
          'api_id' => $api_id,
          'api_name' => $api_read[$api_id] ?? $api_id,
          'provider_id' => $provider->id(),
          'timestamp' => time(),
          'ai_verdict' => $result['ai_verdict'] ?? '',
          'ai_metadata' => isset($result['ai_metadata']) ? json_encode($result['ai_metadata']) : '',
        ];

        if ($result['state'] === 'fulfilled') {
          $plugin = $active_probes[$api_id] ?? NULL;
          /** @var \GuzzleHttp\Psr7\Response $current_resp */
          $current_resp = $result['value'];

          $is_valid = $plugin
            ? $plugin->validateResponse($current_resp)
            : ($current_resp->getStatusCode() === 200);

          // If AI says CRITICAL, we might want to override validity or just alert
          if (isset($result['ai_verdict']) && $result['ai_verdict'] === 'CRITICAL') {
            $this->alertManager->notify($api_id, 'AI_CRITICAL_ALERT', [
              'issues' => $result['ai_metadata']['issues'] ?? [],
              'summary' => $result['ai_metadata']['analysis_summary'] ?? '',
            ]);
          }

          $failure_key = "api_monitor.failures.$api_id";
          $consecutive_failures = $this->state->get($failure_key, 0);

          if ($is_valid) {
            if ($consecutive_failures) {
              $this->state->set($failure_key, 0);
              $this->alertManager->notify($api_id, 'RECOVERY', []);
            }
          }
          else {
            $consecutive_failures++;
            $this->state->set($failure_key, $consecutive_failures);
            $critical_count_alert = $plugin ? $plugin->getCriticalAlertCount() : 3;
            if ($consecutive_failures === $critical_count_alert) {
              $this->alertManager->notify($api_id, 'CRITICAL_DOWN', ['code' => $current_resp->getStatusCode()]);
            }
          }

          $threshold = $plugin ? $plugin->getUptimeThreshold() : 95;
          $current_uptime = $this->getUptime($api_id, 48);
          if ($current_uptime < $threshold) {
            $this->alertManager->notify($api_id, 'UPTIME_LOW', ['uptime' => $current_uptime]);
          }

          $data['status_code'] = $current_resp->getStatusCode();
          $data['success'] = $is_valid ? 1 : 0;
          $data['response_time'] = round($response_time, 2);
        }
        else {
          $data['success'] = 0;
        }

        $history_id = $this->database->insert('api_monitor_history')->fields($data)->execute();
        $this->moduleHandler->invokeAll('api_monitor_history_insert', [$history_id, $result, $api_id, $provider->id()]);
      }
    }

    $this->purgeOldRecords();
    $this->invalidateStatusCache();
  }

  /**
   * Delete records older than 30 days.
   */
  protected function purgeOldRecords() {
    $days = $this->configFactory->get('api_monitor_client.settings')
      ->get('history_days') ?: 30;
    $expiry = time() - ($days * 86400);
    $this->database->delete('api_monitor_history')
      ->condition('timestamp', $expiry, '<')
      ->execute();
  }

  /**
   * Invalidate the status page cache.
   */
  protected function invalidateStatusCache() {
    Cache::invalidateTags(['api_monitor:status_page']);
  }

  /**
   * Retrieves the latest API records from the database.
   *
   * @param string|null $provider_id
   *   The provider ID filter.
   * @param int $limit
   *   Maximum number of records to retrieve.
   *
   * @return array|null
   *   An array of the latest API records, or an empty array if no records
   *   are found or an error occurs.
   */
  public function retrieveApiRecords(?string $provider_id = NULL, int $limit = 50): ?array {
    try {
      $subquery = $this->database->select('api_monitor_history', 'mh2');
      $subquery->addField('mh2', 'api_id');
      $subquery->addExpression('MAX(timestamp)', 'max_ts');
      $subquery->groupBy('api_id');

      $query = $this->database->select('api_monitor_history', 'h');
      $query->join(
        $subquery,
        'last_checks',
        'h.api_id = last_checks.api_id AND h.timestamp = last_checks.max_ts'
      );
      $query->fields('h');

      if ($provider_id) {
        $query->condition('h.provider_id', $provider_id);
      }

      $query->orderBy('h.timestamp', 'DESC');
      $query->range(0, $limit);

      return $query->execute()->fetchAll() ?: [];
    }
    catch (\Throwable $e) {
      $this->logger->error($e->getMessage());
      return [];
    }
  }

  /**
   * Retrieves the uptime percentage for a specific API.
   *
   * @param string $api_id
   *   Endpoint Plugin ID.
   * @param int $hours
   *   Hours calculate upTime api.
   *
   * @return float
   *   The uptime percentage, or 0 if an error occurs.
   */
  public function getUptime(string $api_id, int $hours = 24): float {
    $time_threshold = time() - ($hours * 3600);
    $uptime_query = $this->database->select('api_monitor_history', 'h')
      ->fields('h', ['api_id'])
      ->condition('timestamp', $time_threshold, '>=')
      ->condition('api_id', $api_id);
    $uptime_query->addExpression('AVG(success) * 100', 'uptime_percentage');
    $uptime_query->groupBy('api_id');

    $uptime_data = $uptime_query->execute()->fetchAllKeyed(0, 1);
    return $uptime_data[$api_id] ?? 0;
  }

  /**
   * Permette di impostare manualmente la collection (utile per i Test).
   *
   * @param array $collection
   *   Static collection API for analyze.
   */
  public function setDiscoveryCollection(array $collection): void {
    $this->discoveryCollection = $collection;
  }

  /**
   * Retrieves the API discovery information from all providers.
   *
   * @return array
   *   An array of all APIs from all providers.
   */
  public function getAllDiscoveries(): array {
    /** @var \Drupal\api_monitor_client\Entity\ApiMonitorProviderInterface[] $providers */
    $providers = $this->entityTypeManager->getStorage('api_monitor_provider')->loadMultiple();
    $all_apis = [];
    foreach ($providers as $provider) {
      $discovery = $this->getApiDiscovery($provider);
      if ($discovery) {
        foreach ($discovery as $api) {
          $api['provider_id'] = $provider->id();
          $all_apis[] = $api;
        }
      }
    }
    return $all_apis;
  }

  /**
   * Retrieves manual endpoints for a provider.
   *
   * @param \Drupal\api_monitor_client\Entity\ApiMonitorProviderInterface $provider
   *   The provider entity.
   *
   * @return array
   *   Array of manual endpoint definitions.
   */
  public function getManualEndpoints(ApiMonitorProviderInterface $provider): array {
    $storage = $this->entityTypeManager->getStorage('api_monitor_manual');
    $query = $storage->getQuery()
      ->condition('provider', $provider->id())
      ->condition('status', 1)
      ->accessCheck(FALSE);

    $ids = $query->execute();
    if (empty($ids)) {
      return [];
    }

    $endpoints = [];
    /** @var \Drupal\api_monitor_client\Entity\ApiMonitorManualInterface $entity */
    foreach ($storage->loadMultiple($ids) as $entity) {
      $endpoints[] = [
        'id' => $entity->getApiId(),
        'label' => $entity->label(),
        'path' => $entity->getEndpointPath(),
        'methods' => [$entity->getMethod()],
        'auth_required' => $entity->getAuthType() !== 'none',
        'auth_type' => $entity->getAuthType(),
        'auth_credentials' => $entity->getAuthCredentials(),
        'auth_header_name' => $entity->getAuthHeaderName(),
        'headers' => $entity->getHeaders(),
        'body' => $entity->getBody(),
        'probe_plugin' => $entity->getProbePlugin(),
        'manual' => TRUE,
      ];
    }

    return $endpoints;
  }

  /**
   * Fetches discovery from remote API.
   *
   * @param \Drupal\api_monitor_client\Entity\ApiMonitorProviderInterface $provider
   *   The provider entity.
   *
   * @return array|null
   *   Discovery data or NULL if failed.
   */
  protected function fetchRemoteDiscovery(ApiMonitorProviderInterface $provider): ?array {
    $token = $provider->isAuthEnabled() ? $this->getAccessToken($provider) : NULL;

    $cid = 'api_monitor_client:discovery:' . $provider->id();
    if ($cache = $this->cache->get($cid)) {
      return $cache->data;
    }

    try {
      if ($provider->isAuthEnabled() && !$token) {
        $this->logger->error('Unable to obtain OAuth token for provider ' . $provider->label());
        return NULL;
      }

      $baseUrl = $provider->getProviderUrl();
      $discovery_endpoint = $provider->getDiscoveryEndpoint();

      // If no discovery endpoint configured, skip remote discovery
      if (empty($discovery_endpoint)) {
        return NULL;
      }

      $request_options = [
        'timeout' => 10,
        'headers' => [
          'Content-Type' => 'application/json',
        ],
      ];

      if ($token) {
        $request_options['headers']['Authorization'] = 'Bearer ' . $token;
      }

      $response = $this->httpClient->request('GET', $baseUrl . $discovery_endpoint, $request_options);
      if ($response->getStatusCode() !== 200) {
        return NULL;
      }

      $data = json_decode($response->getBody()->getContents(), TRUE);
      // Mark as remote discovery
      foreach ($data as &$item) {
        $item['manual'] = FALSE;
      }

      $this->cache->set($cid, $data, time() + 3600, ['api_monitor:discovery']);
      return $data;
    }
    catch (\Exception $e) {
      $this->logger->warning('Discovery failed for provider @provider: @message', [
        '@provider' => $provider->label(),
        '@message' => $e->getMessage(),
      ]);
      return NULL;
    }
  }

  /**
   * Retrieves the API discovery information.
   *
   * @param \Drupal\api_monitor_client\Entity\ApiMonitorProviderInterface|null $provider
   *   The provider entity.
   *
   * @return array|null
   *   Array of API definitions or NULL.
   */
  public function getApiDiscovery(?ApiMonitorProviderInterface $provider = NULL): ?array {
    if (!empty($this->discoveryCollection)) {
      return $this->discoveryCollection;
    }

    if (!$provider) {
      return NULL;
    }

    // Try automatic discovery first
    $discovery = $this->fetchRemoteDiscovery($provider);

    // Fallback to manual endpoints
    if (empty($discovery)) {
      $discovery = $this->getManualEndpoints($provider);
    }

    return $discovery ?: NULL;
  }

  /**
   * Returns the probe manager service.
   *
   * @return \Drupal\api_monitor_client\ApiMonitorProbeManager
   *   The probe manager.
   */
  public function getProbeManager(): ApiMonitorProbeManager {
    return $this->probeManager;
  }

}
