<?php

namespace Drupal\acquia_contenthub\Client;

use Acquia\ContentHubClient\CDF\CDFObject;
use Acquia\ContentHubClient\ContentHubClient;
use Acquia\ContentHubClient\ContentHubLoggingClient;
use Acquia\ContentHubClient\Settings;
use Acquia\Hmac\Exception\KeyNotFoundException;
use Acquia\Hmac\KeyLoader;
use Acquia\Hmac\RequestAuthenticator;
use Drupal\acquia_contenthub\Exception\EventServiceUnreachableException;
use Drupal\acquia_contenthub\Libs\Traits\HandleResponseTrait;
use Drupal\acquia_contenthub\Libs\Traits\ResponseCheckerTrait;
use Drupal\acquia_contenthub\Settings\ConnectionDetailsInterface;
use Drupal\acquia_contenthub\Settings\ContentHubConfigurationInterface;
use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Extension\ModuleExtensionList;
use Psr\Log\LoggerInterface;
use Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * Instantiates an Acquia ContentHub Client object.
 *
 * @see \Acquia\ContentHubClient\ContentHub
 */
class ClientFactory {

  use HandleResponseTrait;
  use ResponseCheckerTrait;

  /**
   * The event dispatcher.
   *
   * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
   */
  protected EventDispatcherInterface $dispatcher;

  /**
   * The contenthub client object.
   *
   * @var \Acquia\ContentHubClient\ContentHubClient
   */
  protected ContentHubClient $client;

  /**
   * Content Hub Event logging client.
   *
   * @var \Acquia\ContentHubClient\ContentHubLoggingClient
   */
  protected ContentHubLoggingClient $loggingClient;

  /**
   * ACH Logger Channel.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected LoggerInterface $logger;

  /**
   * The module extension list.
   *
   * @var \Drupal\Core\Extension\ModuleExtensionList
   */
  protected ModuleExtensionList $moduleList;

  /**
   * Acquia ContentHub connection details.
   *
   * @var \Drupal\acquia_contenthub\Settings\ConnectionDetailsInterface
   */
  protected ConnectionDetailsInterface $achConnectionDetails;

  /**
   * Acquia ContentHub configurations.
   *
   * @var \Drupal\acquia_contenthub\Settings\ContentHubConfigurationInterface
   */
  protected ContentHubConfigurationInterface $achConfigurations;

  /**
   * Psr7 http message factory.
   *
   * @var \Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface
   */
  protected HttpMessageFactoryInterface $httpMessageFactory;

  /**
   * Client metadata manager.
   *
   * @var \Drupal\acquia_contenthub\Client\ClientMetaDataBuilder
   */
  protected ClientMetaDataBuilder $clientMetaDataBuilder;

  /**
   * ClientManagerFactory constructor.
   *
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
   *   The event dispatcher.
   * @param \Psr\Log\LoggerInterface $logger
   *   ACH logger channel.
   * @param \Drupal\Core\Extension\ModuleExtensionList $module_list
   *   The module extension list.
   * @param \Drupal\acquia_contenthub\Settings\ContentHubConfigurationInterface $ach_config
   *   CH configuration factory.
   * @param \Symfony\Bridge\PsrHttpMessage\HttpMessageFactoryInterface $http_message_factory
   *   The psr7 http message factory.
   * @param \Drupal\acquia_contenthub\Client\ClientMetaDataBuilder $client_meta_data_builder
   *   Client metadata builder.
   */
  public function __construct(EventDispatcherInterface $dispatcher, LoggerInterface $logger, ModuleExtensionList $module_list, ContentHubConfigurationInterface $ach_config, HttpMessageFactoryInterface $http_message_factory, ClientMetaDataBuilder $client_meta_data_builder) {
    $this->dispatcher = $dispatcher;
    $this->logger = $logger;
    $this->moduleList = $module_list;
    $this->achConnectionDetails = $ach_config->getConnectionDetails();
    $this->httpMessageFactory = $http_message_factory;
    $this->achConfigurations = $ach_config;
    $this->clientMetaDataBuilder = $client_meta_data_builder;
  }

  /**
   * Instantiates the content hub client.
   *
   * @param \Acquia\ContentHubClient\Settings|null $settings
   *   The Settings object or null.
   *
   * @return \Acquia\ContentHubClient\ContentHubClient|bool
   *   The ContentHub Client
   *
   * @throws \Exception
   */
  public function getClient(?Settings $settings = NULL) {
    if (!$settings) {
      if (isset($this->client)) {
        return $this->client;
      }
      $settings = $this->achConfigurations->getSettings();
    }

    if (!$this->achConfigurations->isConfigurationSet($settings)) {
      return FALSE;
    }

    // Override configuration.
    $languages_ids = array_keys(\Drupal::languageManager()->getLanguages());
    $languages_ids[] = CDFObject::LANGUAGE_UNDETERMINED;

    $config = [
      'base_url' => $settings->getUrl(),
      'client-languages' => $languages_ids,
      'client-user-agent' => $this->getClientUserAgent(),
    ];

    $this->client = new ContentHubClient(
      $this->logger,
      $settings,
      $settings->getMiddleware(),
      $this->dispatcher,
      $config
    );

    return $this->client;
  }

  /**
   * Instantiates the content hub logging client.
   *
   * @return \Acquia\ContentHubClient\ContentHubLoggingClient|null
   *   The ContentHub Logging Client
   *
   * @throws \Exception
   */
  public function getLoggingClient(?Settings $settings = NULL): ?ContentHubLoggingClient {
    if (!$settings) {
      if (isset($this->loggingClient)) {
        return $this->loggingClient;
      }
      $settings = $this->achConfigurations->getSettings();
    }

    // If any of these variables is empty, then we do NOT have a valid
    // connection.
    // @todo add validation for the Hostname.
    if (!$settings
      || !Uuid::isValid($settings->getUuid())
      || empty($settings->getName())
      || empty($settings->getUrl())
      || empty($settings->getApiKey())
      || empty($settings->getSecretKey())
    ) {
      return NULL;
    }

    // The event service URL should be retrievable automatically, without
    // needing to fetch it first.
    if (!$this->getLoggingUrl()) {
      // This suggests connection issues.
      return NULL;
    }

    $config = [
      'base_url' => $this->getLoggingUrl(),
      'client-user-agent' => $this->getClientUserAgent(),
    ];

    $this->loggingClient = new ContentHubLoggingClient(
      $this->logger,
      $settings,
      $settings->getMiddleware(),
      $this->dispatcher,
      $config
    );

    $this->checkLoggingClient($this->loggingClient);

    return $this->loggingClient;
  }

  /**
   * Checks whether Logging Client is reachable or not.
   *
   * @param \Acquia\ContentHubClient\ContentHubLoggingClient|null $logging_client
   *   Logging client.
   *
   * @throws \Exception
   */
  public function checkLoggingClient(?ContentHubLoggingClient $logging_client = NULL): void {
    if (is_null($logging_client)) {
      throw new \RuntimeException('Content Hub Logging Client is not configured.');
    }

    try {
      $resp = $logging_client->ping();
    }
    catch (\Exception $e) {
      throw new EventServiceUnreachableException(sprintf('Error during contacting Event Micro Service: %s', $e->getMessage()));
    }
    if (!$this->isSuccessful($resp)) {
      throw new EventServiceUnreachableException(sprintf('Content Hub Logging Client could not reach Event Micro Service: status code: %s, body: %s', $resp->getStatusCode(), $resp->getBody()));
    }
  }

  /**
   * Returns Client's user agent.
   *
   * @return string
   *   User Agent.
   */
  protected function getClientUserAgent() {
    // Find out the module version in use.
    $module_info = $this->moduleList->getExtensionInfo('acquia_contenthub');
    $module_version = (isset($module_info['version'])) ? $module_info['version'] : '0.0.0';
    $drupal_version = (isset($module_info['core'])) ? $module_info['core'] : '0.0.0';

    return 'AcquiaContentHub/' . $drupal_version . '-' . $module_version;
  }

  /**
   * Makes a call to get a client response based on the client name.
   *
   * Note, this receives a Symfony request, but uses a PSR7 Request to Auth.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   Request.
   *
   * @return \Acquia\Hmac\KeyInterface|bool
   *   Authentication Key, FALSE otherwise.
   */
  public function authenticate(Request $request) {
    if (!$this->getClient()) {
      return FALSE;
    }

    $keys = [
      $this->client->getSettings()->getApiKey() => $this->client->getSettings()->getSecretKey(),
      'Webhook' => $this->client->getSettings()->getSharedSecret(),
    ];
    $keyLoader = new KeyLoader($keys);

    $authenticator = new RequestAuthenticator($keyLoader);

    $psr7_request = $this->httpMessageFactory->createRequest($request);

    try {
      return $authenticator->authenticate($psr7_request);
    }
    catch (KeyNotFoundException $exception) {
      $this->logger
        ->debug('HMAC validation failed. [authorization_header = {authorization_header}', [
          'authorization_header' => $request->headers->get('authorization'),
        ]);
    }

    return FALSE;
  }

  /**
   * Wrapper for register method.
   *
   * @param string $name
   *   The client name.
   * @param string $url
   *   The content hub api hostname.
   * @param string $api_key
   *   The api key.
   * @param string $secret
   *   The secret key.
   * @param string $api_version
   *   The api version, default v1.
   *
   * @return \Acquia\ContentHubClient\ContentHubClient
   *   Return Content Hub client.
   *
   * @throws \Exception
   *
   * @see \Acquia\ContentHubClient\ContentHubClient::register()
   */
  public function registerClient(string $name, string $url, string $api_key, string $secret, string $api_version = 'v2'): ContentHubClient {
    $client_metadata = $this->clientMetaDataBuilder->buildClientMetadata();
    $client = ContentHubClient::register($this->logger, $this->dispatcher, $name, $url, $api_key, $secret, $client_metadata, $api_version);
    $this->client = $client;
    return $this->client;
  }

  /**
   * Returns Event logging URL for given Content Hub Service URL.
   *
   * @return string
   *   Event Logging Url to use for logging. Varies with CH realm.
   *
   * @throws \Exception
   */
  public function getLoggingUrl(): string {
    if (!$this->achConnectionDetails->getEventServiceUrl()) {
      $event_logging_url = $this->getEventLoggingUrlFromRemoteSettings();
      if (!$event_logging_url) {
        return '';
      }
      $this->achConnectionDetails->setEventServiceUrl($event_logging_url);
    }

    return $this->achConnectionDetails->getEventServiceUrl();
  }

  /**
   * Fetch event logging url from ACH remote settings.
   *
   * @return string
   *   Event logging url.
   *
   * @throws \ReflectionException
   * @throws \Exception
   */
  public function getEventLoggingUrlFromRemoteSettings(): string {
    $client = $this->getClient();
    if (!$client) {
      return '';
    }
    $remote_settings = $client->getRemoteSettings();
    if (isset($remote_settings['event_service_url'])) {
      return $remote_settings['event_service_url'];
    }

    $this->logger->error('Event logging service url not found in remote settings.');
    return '';
  }

}
