<?php

namespace Drupal\acquia_contenthub\Controller;

use Drupal\acquia_contenthub\AcquiaContentHubEvents;
use Drupal\acquia_contenthub\Client\ClientFactory;
use Drupal\acquia_contenthub\Event\HandleWebhookEvent;
use Drupal\Component\Render\FormattableMarkup;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Controller\ControllerBase;
use GuzzleHttp\Psr7\Response;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpFoundation\Request;

/**
 * Controller for ContentHub webhooks.
 */
class ContentHubWebhookController extends ControllerBase {

  /**
   * Webhook V2.
   */
  public const WEBHOOK_V2 = '2.0';

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

  /**
   * Content Hub Client Factory.
   *
   * @var \Drupal\acquia_contenthub\Client\ClientFactory
   */
  protected $clientFactory;

  /**
   * WebhooksSettingsForm constructor.
   *
   * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $dispatcher
   *   The event dispatcher.
   * @param \Drupal\acquia_contenthub\Client\ClientFactory $client_factory
   *   The client factory.
   */
  public function __construct(EventDispatcherInterface $dispatcher, ClientFactory $client_factory) {
    $this->dispatcher = $dispatcher;
    $this->clientFactory = $client_factory;
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('event_dispatcher'),
      $container->get('acquia_contenthub.client.factory')
    );
  }

  /**
   * Process an incoming webhook from the ContentHub Service.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   A Symfony request object.
   *
   * @return mixed
   *   The response.
   */
  public function receiveWebhook(Request $request) {
    // If you're using a host re-writer, you need to find the original host.
    if ($request->headers->has('x-original-host')) {
      $request->headers->set('host', $request->headers->get('x-original-host'));
    }
    $payload = $request->getContent();
    // If payload is empty, return with 403 status code.
    if (empty($payload)) {
      return new Response(400);
    }

    $decoded_payload = $this->setWebhookType(json_decode($payload, TRUE));
    $webhook_v2 = $this->isWebhookV2($decoded_payload);
    if (!$webhook_v2) {
      $decoded_payload = $this->setWebhookReason($decoded_payload);
    }

    try {
      $key = $this->validateWebhookSignature($request);
      if ($key) {
        // Notify about the arrival of the webhook request.
        $this->getLogger('acquia_contenthub')->debug('Webhook landing: ' . $this->beautifyLogMessage($decoded_payload));

        if ($payload = Json::decode($payload)) {
          $event = new HandleWebhookEvent($request, $payload, $key, $this->clientFactory->getClient(), $webhook_v2);
          $this->dispatcher->dispatch($event, AcquiaContentHubEvents::HANDLE_WEBHOOK);
          return $event->getResponse();
        }
      }
      else {
        $ip_address = $request->getClientIp();
        $message = new FormattableMarkup('Webhook [from IP = @IP] rejected (Signatures do not match): @whook', [
          '@IP' => $ip_address,
          '@whook' => print_r($payload, TRUE),
        ]);
        $this->getLogger('acquia_contenthub')->debug($message);
      }
    }
    catch (\Exception $e) {
      $ip_address = $request->getClientIp();
      $message = new FormattableMarkup('Webhook [from IP = @IP] rejected. @error', [
        '@IP' => $ip_address,
        '@error' => print_r($e->getMessage(), TRUE),
      ]);
      $this->getLogger('acquia_contenthub')->debug($message);
    }

    return new Response();
  }

  /**
   * Checks if webhook version is 2.0.
   *
   * @param array $decoded_payload
   *   Decoded array of payload.
   *
   * @return bool
   *   True if webhook version 2, otherwise False.
   */
  public function isWebhookV2(array $decoded_payload): bool {
    return !empty($decoded_payload['payload_version']) && $decoded_payload['payload_version'] === self::WEBHOOK_V2;
  }

  /**
   * Sets webhook-type as manual or automatic in payload.
   *
   * @param array $payload
   *   The payload.
   *
   * @return array
   *   The payload with additional attributes.
   */
  public function setWebhookType(array $payload): array {
    $payload['webhook-type'] = empty($payload['uuid']) || !Uuid::isValid($payload['uuid']) ? 'Manual' : 'Automatic';
    return $payload;
  }

  /**
   * Sets webhook-reason in payload.
   *
   * @param array $payload
   *   The payload.
   *
   * @return array
   *   The payload with additional attributes.
   */
  public function setWebhookReason(array $payload): array {
    if (empty($payload['reason'])) {
      $payload['reason'] = 'Manual';
    }
    else {
      $payload['reason'] = empty($payload['uuid']) || !Uuid::isValid($payload['uuid']) ? ('Filter Uuid(s) : ' . $payload['reason']) : $payload['reason'];
    }
    return $payload;
  }

  /**
   * Webhook landing messages beautified.
   *
   * @param array $payload
   *   The payload.
   *
   * @return string
   *   The request body content converted to string.
   */
  public function beautifyLogMessage(array $payload): string {
    $message = [];
    foreach ($payload as $key => $value) {
      if (is_array($value)) {
        $message[] = sprintf('[%s: { %s }]', ucfirst($key), $this->beautifyLogMessage($value));
        continue;
      }
      $message[] = sprintf('[%s: "%s"]', ucfirst($key), $value);
    }
    return implode(', ', $message);
  }

  /**
   * Validates a webhook signature.
   *
   * @param \Symfony\Component\HttpFoundation\Request $request
   *   The request.
   *
   * @return bool|\Acquia\Hmac\KeyInterface
   *   TRUE if signature verification passes, FALSE otherwise.
   */
  public function validateWebhookSignature(Request $request) {
    return $this->clientFactory->authenticate($request);
  }

}
